lostars
发布于 2018-11-20 / 2275 阅读
0

EFFECTIVE JAVA 3RD EDITION — 第六章 枚举与注解

Item 34 : Use enums instead of int constants

使用枚举来代替整型常量

枚举类型添加以前使用常量的方式来满足使用需要,但是这种方式有很多缺点:
无法保证类型安全,并且没有没有表现力(不够优雅)。
常量是跟随调用者编译的,如果常量改变了但是调用者没有重新编译那么会出现问题
不利于debug,因为你打印出来的都是一些常量。
枚举保证了一个编译时类型安全,避免出现枚举滥用错用的情况;同时枚举是单例,并且无法扩展,这也保证了枚举的安全性。
同时为了获取常量值,需要定义一系列方法和构造器来供外部访问,同时为了方便debug可以重写 toString 方法。
如果有需要,可以让枚举私有、包内私有化或者作为内部类,自然如果需要枚举的共用或者重用可以把他放在顶级包内。

Item 35 : Use instance fields instead of ordinals

使用常量属性来代替枚举序号

避免使用 ordinal 方法,因为枚举的修改都会导致不可预期的序号变换;
大部分程序员都不需要这样的一个功能,除非是一个特殊的数据结构比如 Enumset 和 EnumMap 需要遍历枚举的时候。

Item 36 : Use EnumSet instead of bit fields

使用枚举集合来代替位属性

比如下面的代码:

// Bit field enumeration constants - OBSOLETE! 
public class Text {    
    public static final int STYLE_BOLD          = 1 << 0;  // 1    
    public static final int STYLE_ITALIC        = 1 << 1;  // 2    
    public static final int STYLE_UNDERLINE     = 1 << 2;  // 4    
    public static final int STYLE_STRIKETHROUGH = 1 << 3;  // 8    
    // Parameter is bitwise OR of zero or more STYLE_ constants    
    public void applyStyles(int styles) { ... } 
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

这种写法无法直观的修改和检查值,并且也很难去遍历常量,
同时你并不能很简单的去检查数据是否溢出,
并且一旦确定常量就很难再去作修改,除非修改你的API。

这个时候使用枚举集合就可以很好的解决这个问题:

// EnumSet - a modern replacement for bit fields 
public class Text {    
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }    
    // Any Set could be passed in, but EnumSet is clearly best    
    public void applyStyles(Set<Style> styles) { ... } 
} 
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC)); 

很直观并且清晰。
注意上面的方法参数是 Set<Style> 而不是 EnumSet,这种时候方法参数最好接收一个接口类而不是他的实现,
这样就可以给接口的各种实现提供尽可能多的支持(item 64)
总的来说就是需要在集合中使用枚举的时候没有理由用位属性来替代EnumSet,
EnumSet唯一缺点就是无法在java9之前创建一个不变的EnumSet,
但可以封装 Collections.unmodifiableSet 方法到EnumSet中,不过这样会破坏EnumSet的简洁和性能。

Item 37 : Use EnumMap instead of ordinal indexing

使用枚举map来代替枚举序号索引

用书中一个很典型的例子来进一步说明:

// 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()];
        }
    }
}

用Phase来表示物体的三种状态(固态,液态,气态),同时 Transition 来表示每种状态间转换过程,定义一个二维数组来表示,
方法from返回任意两种状态之间的转换过程。

代码看着是很美好,也很简短。

首先在二维数组中有null容易导致空指针,如果再加一个状态PLASMA(等离子态),
代码修改就比较麻烦,特别是二维数组的维护(顺序错误的话就会返回错误的结果)。

使用EnumMap来实现:

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 = 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, t -> t,
            (x, y) -> y, () -> new EnumMap<>(Phase.class))));
        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
    }
}

Transition维护的是枚举(转换过程的枚举),列出了所有的转换状态,
内部属性m则初始化存储了一个 Map<Phase, EnumMap<Phase, Transition>>,
初始化方法调用的是java8的流方法来对m作一个初始化,
Phase作为key,value存储的则是该状态转换为其他状态的所有过程。
from方法就只需要get返回即可。

这样写虽然代码量增加了,但是可维护性提升了,如果需要增加一个PLASMA,
只需要在PHASE中增加枚举,同时Transition中增加可能的转换过程即可。
上面的代码基本不可能给你犯错的机会,除非你连固态到气态的过程都能搞错。

总的来说不要使用枚举的 ordinal 去作为数组索引,而应该使用EnumMap。

Item 38 : Emulate extensible enums with interfaces

使用接口来增加枚举的拓展性

这一节主要讲的是枚举的一个扩展性,由于枚举是不能被继承的,导致扩展性很差
如果有扩展的需要考虑将扩展点提取到一个接口中,枚举实现该接口,其他的枚举按照需要实现该接口即可。

Item 39 : Prefer annotations to naming patterns

使用注解而不是命名模式

命名模式的缺点(让我想起了JPA):

  1. 命名容易出现错误;比如junit4之前的版本测试方法必须以test开头,这样很容易出现命名错误(tset),你也只有发现测试方法没有执行才会知道命名错了。
  2. 并不能保证作用在了正确的代码上;比如你创建了一个 TestSafetyMechanisms 类希望Junit3能够测试其内部的所有方法,但事实上并不会执行非test开头的方法。
  3. 不能很好的结合参数和程序代码;比如需要测试在抛出某一个异常的时候通过测试,这时候就需要将异常作为参数传入测试方法,再进行判断。
    Java8中引入了可重复注解 Repeatable:
    重复注解只使用在注解字段中存在数组的情况,同时 Repeatable 注解必须指定可以重复的注解,该注解只包含一个数组字段;
    getAnnotationsByType 方法可以同时获取重复和非重复注解;
    使用 isAnnotationPresent 方法判断是否有某个注解时需要格外注意:
    如果参数传入实际写在代码中的注解,则会忽略只包含一个数组字段的注解(真正意义上重复的注解),
    如果参数传入上面后者,则会忽略上面前者;
    所以使用该方法时需要判断两个注解是否存在。
    重复注解的出现旨在提升代码可读性,看需求使用。

如果代码需要调用者传入参数到源码中就使用注解而不是去使用命名模式。
虽然大部分的编码都不需要定义注解,但是java内置的一些注解还是应该使用的(比如 Override),
这些注解已经成立了标准使用之前还是需要多看看。

Item 40 : Consistently use the Override annotation

坚持使用Override注解

如果不使用 Override 注解,比如 equals 方法,就会出现一些意外情况,使用注解同时会帮助你检查是否正确的覆盖了父类的方法。
大部分IDE都会在你实现接口或者覆盖父类方法的同时自动加上注解。
当然如果覆盖了一个抽象方法,这个时候你就没必要添加 Override 注解了。

Item 41 : Use marker interfaces to define types

使用标记接口来定义类型

标记接口定义:没有声明任何方法的接口,比如Java api中的Serializable,表示某个实现该接口的对象可以被序列化。
标记注解定义类似,两者都只是作一个标记,表明某个功能或用途。

标记接口相对于标记注解的好处:

  1. 标记接口定义了一个可以被标记类所实现的类型(因为是接口),标记注解不能;
  2. 标记接口可以被更精确的定位;如果使用的是一个标记注解(ElementType.TYPE),它可以被使用到任何可以使用的地方,
    而标记接口则可以只继承(只让它适用)该接口,让所有标记类都实现该接口,从而达到可适用的目的。比如 Set 接口则是一个限制的标记接口,它只适用于 Collection 的子类。
    而标记注解相对标记接口的优点则是:可在一个注解集成的框架中使用。

那么如何正确的使用它们?
如果标记是作用于元素(属性)的话肯定是使用注解,
而作用于类或者是接口,并且会写一些只接收标记对象方法的话则使用标记接口,这样就会让你传入接口作为参数,同时在编译期间做了类型检查。

当你定义的注解目标是 ElementType.TYPE 的时候,则需要思考使用哪个才是更为合适。


这一章介绍的两个主要内容:
枚举,需要谨慎使用ordinal方法,多考虑使用EnumSet EnumMap;
注解,简单介绍使用方式;