第六章 枚举和注解
34. 用枚举类型代替 int 常量
int枚举
int枚举类型中有一系列public static final int常量,每个常量表示一个类型。
它有许多缺点:
- 不提供类型安全性,容易误用(入参是Apple常量,但是传任何int都可以)
- 常量值修改,客户端也必须修改
- 调试麻烦
枚举类型
Java 的枚举类型是成熟的类,其他语言中的枚举类型本质上是 int 值。
枚举优势:
-
实例受控
Java 枚举类型通过 public static final 修饰的字段为每个枚举常量导出一个实例。枚举类型实际上是 final 类型,因为构造函数是私有的。客户端既不能创建枚举类型的实例,也不能继承它,所以除了声明的枚举常量之外,不能有任何实例。换句话说,枚举类型是实例受控的类(Item-1)。它们是单例(Item-3)的推广应用,单例本质上是单元素的枚举。 -
提供编译时类型安全性。
如果将参数声明为 Apple 枚举类型,则可以保证传递给该参数的任何非空对象引用都是 Apple 枚举值之一。尝试传递错误类型的值将导致编译时错误,将一个枚举类型的表达式赋值给另一个枚举类型的变量,或者使用 == 运算符比较不同枚举类型的值同样会导致错误。 -
名称相同的枚举类型常量能共存
名称相同的枚举类型常量能和平共存,因为每种类型都有自己的名称空间。你可以在枚举类型中添加或重新排序常量,而无需重新编译其客户端,因为导出常量的字段在枚举类型及其客户端之间提供了一层隔离:常量值不会像在 int 枚举模式中那样编译到客户端中。最后,你可以通过调用枚举的 toString 方法将其转换为可打印的字符串。 -
允许添加任意方法和字段并实现任意接口
枚举类型允许添加任意方法和字段并实现任意接口。它们提供了所有 Object 方法的高质量实现(参阅 Chapter 3),还实现了 Comparable(Item-14)和 Serializable(参阅 Chapter 12),并且它们的序列化形式被设计成能够适应枚举类型的可变性。如果方法是常量特有的,可以在枚举类型中添加抽象方法,在声明枚举实例时覆盖抽象方法// Enum type with constant-specific class bodies and data public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } // Implementing a fromString method on an enum type private static final Map<String, Operation> stringToEnum =Stream.of(values()).collect(toMap(Object::toString, e -> e)); // Returns Operation for string, if any public static Optional<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); } public abstract double apply(double x, double y); }
上述实现的缺点是如果常量特有的方法有可以复用的代码,那么会造成许多冗余。你可以把冗余代码抽出成方法,常量覆盖抽象方法时调用抽出的方法,这种实现同样有许多冗余代码;你也可以把抽象方法改成具体方法,里面是可以复用的代码,添加常量时如果不覆盖那就用默认的,这种实现问题在于添加常量时如果忘记覆盖那就用默认方法了。
当常量特有的方法有可以复用的代码时,我们采用策略枚举模式,将可复用代码移到私有嵌套枚举中。当常量调用方法时,调用私有嵌套枚举常量的方法。
在枚举上实现特定常量的行为时 switch 语句不是一个好的选择,只有在枚举不在你的控制之下,你希望它有一个实例方法来返回每个常量的特定行为,这时候才用 switch
总之,枚举类型相对于 int 常量的优势是毋庸置疑的。枚举更易于阅读、更安全、更强大。许多枚举不需要显式构造函数或成员,但有些枚举则受益于将数据与每个常量关联,并提供行为受数据影响的方法。将多个行为与一个方法关联起来,这样的枚举更少。在这种相对少见的情况下,相对于使用 switch 的枚举,特定常量方法更好。如果枚举常量有一些(但不是全部)共享公共行为,请考虑策略枚举模式。
35. 使用实例字段替代序数
每个枚举常量都有一个 ordinal 方法,返回枚举常量在枚举类中的位置。不要用这个方法返回与枚举常量关联的值,一旦常量位置变动,值就是错的,所以不要用序数生成与枚举常量关联的值,而是将其存在实例字段中
36. 用 EnumSet 替代位字段
位字段
如果枚举类型的元素主要在 Set 中使用,传统上使用 int 枚举模式(Item34),通过不同的 2 的幂次为每个常量赋值:
// 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) { ... }
}
使用位运算的 OR 操作将几个常量组合成一个集合,这个集合叫做位字段:
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
位字段表示方式允许使用位运算高效地执行集合操作,如并集和交集。但是位字段具有 int 枚举常量所有缺点,甚至更多。当位字段被打印为数字时,它比简单的 int 枚举常量更难理解。没有一种简单的方法可以遍历由位字段表示的所有元素。最后,你必须预测在编写 API 时需要的最大位数,并相应地为位字段(通常是 int 或 long)选择一种类型。一旦选择了一种类型,在不更改 API 的情况下,不能超过它的宽度(32 或 64 位)。
EnumSet
一些使用枚举而不是 int 常量的程序员在需要传递常量集合时仍然坚持使用位字段。没有理由这样做,因为存在更好的选择。java.util
包提供 EnumSet 类来有效地表示从单个枚举类型中提取的值集。这个类实现了 Set 接口,提供了所有其他 Set 实现所具有的丰富性、类型安全性和互操作性。但在内部,每个 EnumSet 都表示为一个位向量。如果底层枚举类型有 64 个或更少的元素(大多数都是),则整个 EnumSet 用一个 long 表示,因此其性能与位字段的性能相当。批量操作(如 removeAll 和 retainAll)是使用逐位算法实现的,就像手动处理位字段一样。但是,你可以避免因手工修改导致产生不良代码和潜在错误:EnumSet 为你完成了这些繁重的工作。
当之前的示例修改为使用枚举和 EnumSet 而不是位字段时。它更短,更清晰,更安全:
// 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) { ... }
}
下面是将 EnumSet 实例传递给 applyStyles 方法的客户端代码。EnumSet 类提供了一组丰富的静态工厂,可以方便地创建集合,下面的代码演示了其中的一个:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
总之,如果枚举类型将在 Set 中使用,不要用位字段表示它。 EnumSet 类结合了位字段的简洁性和性能,以及 (Item-34) 中描述的枚举类型的许多优点。EnumSet 的一个真正的缺点是,从 Java 9 开始,它不能创建不可变的 EnumSet,在未来发布的版本中可能会纠正这一点。同时,可以用 Collections.unmodifiableSet
包装 EnumSet,但简洁性和性能将受到影响
37. 使用 EnumMap 替换序数索引
序数索引
当需要将枚举常量映射到其它值时,有一种方法是序数索引,使用 ordinal 方法返回的序数表示key。这种方式存在Item35提到的问题
EnumMap
EnumMap使用枚举常量作为key,功能丰富、类型安全
38. 使用接口模拟可扩展枚举
枚举不可以扩展(继承)另一个枚举,但可以实现接口。可以用接口模拟可扩展的枚举。下面的例子将Item34的枚举类型的Operation改成了接口类型。通过实现接口来模拟继承枚举。
// Emulated extensible enum using an interface
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
// Emulated extension enum
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
可以用接口类型引用指向不同子枚举类型实例。
public static void main(String[] args) {
Operation op = BasicOperation.DIVIDE;
System.out.println(op.apply(15, 3));
op=ExtendedOperation.EXP;
System.out.println(op.apply(2,5));
}
java.nio.file.LinkOption
使用了这个Item描述的方法,它实现了 CopyOption 和 OpenOption 接口。
总之,枚举不可以扩展或被扩展,但是你可以通过接口模拟枚举之间的层级关系
39. 注解优于命名模式
命名模式
用来标明某些程序元素需要工具或框架特殊处理。
例如JUnit3及之前,用户必须写test开头的测试方法,不然测试都不能运行。
缺点:1.写错方法名会导致测试通过,但是实际上根本没有运行;2.不能将参数值和程序元素关联,例如测试方法只有在抛出特定异常时才成功,虽然可以精心设计命名模式,将异常名称写在测试方法名上,但是编译器无法检查异常类是否存在
注解
Junit4 开始使用注解,框架会根据注解执行测试方法,也可以将异常和测试方法绑定。注解本身不修改代码语义,而是通过反射(框架所用的技术)对其特殊处理
40. 坚持使用 @Override 注解
请在要覆盖父类声明的每个方法声明上使用 @Override 注解。只有一个例外,那就是具体类覆盖父类的抽象方法,因为具体类必须要实现父类的抽象方法,所以不必加 @Override 注解
41. 使用标记接口定义类型
标记接口
标记接口是一个空接口,它的作用是标记实现类具有某些属性。例如,实现 Serializable 接口表示类的实例可以写入 ObjectOutputStream(序列化)
标记接口VS标记注解
与标记注解(Item39)相比,标记接口有两个优点:
- 标记接口定义的类型由标记类的实例实现;标记注解不会。因此标记接口类型的存在允许你在编译时捕获错误,标记注解只能在运行时捕获。ObjectOutputStream.writeObject方法入参必须是实现了Serializable的实例,否则会报编译错误(JDK设计缺陷,writeObject入参是Object类型)
- 标记接口相对于标记注解的另一个优点是可以更精确地定位子类型。例如 Set 是一个标记接口,它继承 Collection接口,Set标记了所有它的实现类都具有Collection功能。Set相较于标记接口的特殊之处在于它还细化了Collection方法 add、equals 和 hashCode 的约定
标记注解的优势:与基于使用注解的框架保持一致性
使用规则
如果标记用在类或接口之外的任何程序元素,必须用标记注解;如果框架使用注解,那就用标记注解;其它情况都用标记接口