高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

使用EnumMap替代序数索引

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。
在这里第一时间翻译成中文版。供大家学习分享之用。

37. 使用EnumMap替代序数索引

有时可能会看到使用ordinal方法(条目 35)来索引到数组或列表的代码。 例如,考虑一下这个简单的类来代表一种植物:

class Plant {
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        [this.name](http://this.name) = name;
        this.lifeCycle = lifeCycle;
    }

    @Override public String toString() {
        return name;
    }
}

现在假设你有一组植物代表一个花园,想要列出这些由生命周期组织的植物(一年生,多年生,或双年生)。为此,需要构建三个集合,每个生命周期作为一个,并遍历整个花园,将每个植物放置在适当的集合中。一些程序员可以通过将这些集合放入一个由生命周期序数索引的数组中来实现这一点:

// Using ordinal() to index into an array - DON"T DO THIS!

Set<Plant>[] plantsByLifeCycle =

    (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++)

    plantsByLifeCycle[i] = new HashSet<>();

for (Plant p : garden)

    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);

// Print the results

for (int i = 0; i < plantsByLifeCycle.length; i++) {

    System.out.printf("%s: %s%n",

        Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);

}

这种方法是有效的,但充满了问题。 因为数组不兼容泛型(条目 28),程序需要一个未经检查的转换,并且不会干净地编译。 由于该数组不知道索引代表什么,因此必须手动标记索引输出。 但是这种技术最严重的问题是,当你访问一个由枚举序数索引的数组时,你有责任使用正确的int值; int不提供枚举的类型安全性。 如果你使用了错误的值,程序会默默地做错误的事情,如果你幸运的话,抛出一个ArrayIndexOutOfBoundsException异常。

有一个更好的方法来达到同样的效果。 该数组有效地用作从枚举到值的映射,因此不妨使用Map。 更具体地说,有一个非常快速的Map实现,设计用于枚举键,称为java.util.EnumMap。 下面是当程序重写为使用EnumMap时的样子:

// Using an EnumMap to associate data with an enum

Map<Plant.LifeCycle, Set<Plant>>  plantsByLifeCycle =

    new EnumMap<>(Plant.LifeCycle.class);

for (Plant.LifeCycle lc : Plant.LifeCycle.values())

    plantsByLifeCycle.put(lc, new HashSet<>());

for (Plant p : garden)

    plantsByLifeCycle.get(p.lifeCycle).add(p);

System.out.println(plantsByLifeCycle);

这段程序更简短,更清晰,更安全,运行速度与原始版本相当。 没有不安全的转换; 无需手动标记输出,因为map键是知道如何将自己转换为可打印字符串的枚举; 并且不可能在计算数组索引时出错。 EnumMap与序数索引数组的速度相当,其原因是EnumMap内部使用了这样一个数组,但它对程序员的隐藏了这个实现细节,将Map的丰富性和类型安全性与数组的速度相结合。 请注意,EnumMap构造方法接受键类型的Class对象:这是一个有限定的类型令牌(bounded type token),它提供运行时的泛型类型信息(条目 33)。

通过使用stream(条目 45)来管理Map,可以进一步缩短以前的程序。 以下是最简单的基于stream的代码,它们在很大程度上重复了前面示例的行为:

// Naive stream-based approach - unlikely to produce an EnumMap!

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle)));

这个代码的问题在于它选择了自己的Map实现,实际上它不是EnumMap,所以它不会与显式EnumMap的版本的空间和时间性能相匹配。 为了解决这个问题,使用Collectors.groupingBy的三个参数形式的方法,它允许调用者使用mapFactory参数指定map的实现:

// Using a stream and an EnumMap to associate data with an enum

System.out.println(Arrays.stream(garden)

        .collect(groupingBy(p -> p.lifeCycle,

() -> new EnumMap<>(LifeCycle.class), toSet()))); 

这样的优化在像这样的示例程序中是不值得的,但是在大量使用Map的程序中可能是至关重要的。

基于stream版本的行为与EmumMap版本的行为略有不同。 EnumMap版本总是为每个工厂生命周期生成一个嵌套map类,而如果花园包含一个或多个具有该生命周期的植物时,则基于流的版本才会生成嵌套map类。 因此,例如,如果花园包含一年生和多年生植物但没有两年生的植物,plantByLifeCycle的大小在EnumMap版本中为三个,在两个基于流的版本中为两个。

你可能会看到数组索引(两次)的数组,用序数来表示从两个枚举值的映射。例如,这个程序使用这样一个数组来映射两个阶段到一个阶段转换(phase transition)(液体到固体表示凝固,液体到气体表示沸腾等等):

// Using ordinal() to index array of arrays - DON"T DO THIS!

public enum Phase {

    SOLID, LIQUID, GAS;

    public enum Transition {

        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;

        // Rows indexed by from-ordinal, cols by to-ordinal

        private static final Transition[][] TRANSITIONS = {

            { null,    MELT,     SUBLIME },

            { FREEZE,  null,     BOIL    },

            { DEPOSIT, CONDENSE, null    }

        };

        // Returns the phase transition from one phase to another

        public static Transition from(Phase from, Phase to) {

            return TRANSITIONS[from.ordinal()][to.ordinal()];

        }

    }

}

这段程序可以运行,甚至可能显得优雅,但外观可能是骗人的。 就像前面显示的简单的花园示例一样,编译器无法知道序数和数组索引之间的关系。 如果在转换表中出错或者在修改PhasePhase.Transition枚举类型时忘记更新它,则程序在运行时将失败。 失败可能是ArrayIndexOutOfBoundsExceptionNullPointerException或(更糟糕的)沉默无提示的错误行为。 即使非空条目的数量较小,表格的大小也是phase的个数的平方。

同样,可以用EnumMap做得更好。 因为每个阶段转换都由一对阶段枚举来索引,所以最好将关系表示为从一个枚举(from 阶段)到第二个枚举(to阶段)到结果(阶段转换)的map。 与阶段转换相关的两个阶段最好通过将它们与阶段转换枚举相关联来捕获,然后可以用它来初始化嵌套的EnumMap

// Using a nested EnumMap to associate data with enum pairs

public enum Phase {

   SOLID, LIQUID, GAS;

   public enum Transition {

      MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

      BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

      SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

      private final Phase from;

      private final Phase to;

      Transition(Phase from, Phase to) {

         this.from = from;

         [this.to](http://this.to) = to;

      }

      // Initialize the phase transition map

      private static final Map<Phase, Map<Phase, Transition>>

        m = Stream.of(values()).collect(groupingBy(t -> t.from,

         () -> new EnumMap<>(Phase.class),

         toMap(t -> [t.to](http://t.to), t -> t,

            (x, y) -> y, () -> new EnumMap<>(Phase.class))));

      public static Transition from(Phase from, Phase to) {

         return m.get(from).get(to);

      }

   }

}

初始化阶段转换的map的代码有点复杂。map的类型是Map<Phase, Map<Phase, Transition>>,意思是“从(源)阶段映射到从(目标)阶段到阶段转换映射。”这个map的map使用两个收集器的级联序列进行初始化。 第一个收集器按源阶段对转换进行分组,第二个收集器使用从目标阶段到转换的映射创建一个EnumMap。 第二个收集器((x, y) -> y))中的合并方法未使用;仅仅因为我们需要指定一个map工厂才能获得一个EnumMap,并且Collectors提供伸缩式工厂,这是必需的。 本书的前一版使用显式迭代来初始化阶段转换map。 代码更详细,但可以更容易理解。

现在假设想为系统添加一个新阶段:等离子体或电离气体。 这个阶段只有两个转变:电离,将气体转化为等离子体; 和去离子,将等离子体转化为气体。 要更新基于数组的程序,必须将一个新的常量添加到Phase,将两个两次添加到Phase.Transition,并用新的十六个元素版本替换原始的九元素阵列数组。 如果向数组中添加太多或太少的元素或者将元素乱序放置,那么如果运气不佳:程序将会编译,但在运行时会失败。 要更新基于EnumMap的版本,只需将PLASMA添加到阶段列表中,并将IONIZE(GAS, PLASMA)DEIONIZE(PLASMA, GAS)添加到阶段转换列表中:

// Adding a new phase using the nested EnumMap implementation

public enum Phase {

    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {

        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),

        BOIL(LIQUID, GAS),   CONDENSE(GAS, LIQUID),

        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),

        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);

        ... // Remainder unchanged

    }

}

该程序会处理所有其他事情,并且几乎不会出现错误。 在内部,map的map是通过数组的数组实现的,因此在空间或时间上花费很少,以增加清晰度,安全性和易于维护。

为了简便起见,上面的示例使用null来表示状态更改的缺失(其从目标到源都是相同的)。这不是很好的实践,很可能在运行时导致NullPointerException。为这个问题设计一个干净、优雅的解决方案是非常棘手的,而且结果程序足够长,以至于它们会偏离这个条目的主要内容。

总之,使用序数来索引数组很不合适:改用EnumMap。 如果你所代表的关系是多维的,请使用EnumMap <...,EnumMap <... >>。 应用程序员应该很少使用Enum.ordinal(条目 35),如果使用了,也是一般原则的特例。