Chapter 6 Enums and Annotations
枚举和注解
JAVA supports two special-purpose families of reference types: a kind of class called an enum type, and a kind of interface called an annotation type. This chapter discusses best practices for using these type families.
JAVA 支持两种特殊目的的引用类型成员:一种叫做枚举类型的类和一种叫做注解类型的接口。本章讨论这俩种类型成员的使用的最佳实践。
item 34: Use enums instead of int constants
使用枚举替换整型常量
Java’s enum types are full-fledged classes, far more powerful than their counterparts in these other languages, where enums are essentially int values.
java的枚举是一个功能齐全的类,比其他语言中的对应的强很多,枚举本质上是一个整数值。
To associate data with enum constants, declare instance fields and write a constructor that takes the data and stores it in the fields. Enums are by their nature immutable, so all fields should be final (Item 17). Fields can be public, but it is better to make them private and provide public accessors (Item 16).
为了将数据和枚举常量联系起来,需要声明实例域并编写一个将数据存储在实例域的中构造方法。由于枚举本质上是不可变的,所以其所有域都应该是final的。域可以是public的,但是最好是声明为private并且提供public的访问器。
Some behaviors associated with enum constants may need to be used only from within the class or package in which the enum is defined. Such behaviors are best implemented as private or package-private methods. Each constant then carries with it a hidden collection of behaviors that allows the class or package containing the enum to react appropriately when presented with the constant. Just as with other classes, unless you have a compelling reason to expose an enum method to its clients, declare it private or, if need be, package-private (Item 15).
一些与枚举常量相关的行为可能只需要在定义枚举的类或包中使用。 这些行为最好以私有或私有包方式实现。 然后每个常量携带一个隐藏的行为集合,允许包含枚举的类或包在与常量一起呈现时作出适当的反应。 与其他类一样,除非您有一个令人信服的理由将枚举方法暴露给它的客户端,否则将其声明为私有的(如果需要的话),也可以将其声明为私有包(项目15)。尽量不要暴露枚举类,因为以后的枚举类修改会更佳方便。
If an enum is generally useful, it should be a top-level class; if its use is tied to a specific top-level class, it should be a member class of that top-level class (Item 24). For example, the java.math.RoundingMode enum represents a rounding mode for decimal fractions. These rounding modes are used by the BigDecimal class, but they provide a useful abstraction that is not fundamentally tied to BigDecimal. By making RoundingMode a top-level enum, the library designers encourage any programmer who needs rounding modes to reuse this enum, lead- ing to increased consistency across APIs.
如果一个枚举通常是有用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(条目24)。 例如,java.math.RoundingMode枚举表示十进制小数的舍入模式。 这些舍入模式被BigDecimal类使用,但是它们提供了一个有用的抽象,它并不与BigDecimal有根本的联系。 通过将RoundingMode设置为顶层枚举,库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨API的一致性。
sometimes you need to associate fundamentally different behavior with each constant.
有时你需要关联不同的方法对于不同的枚举对象
- bad case
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
// Do the arithmetic operation represented by this constant
public double apply(double x, double y) {
switch(this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case DIVIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
bad case因为添加新的操作符容易漏掉switch方法,造成运行时异常,而better way则会编译不通过
- better way
// Enum type with constant-specific method implementations
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;}},
IVIDE{public double apply(double x, double y){return x / y;}};
public abstract double apply(double x, double y);
}
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;}},
IVIDE("/"){public double apply(double x, double y){return x / y;}};
public abstract double apply(double x, double y);
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
}
Enum types have an automatically generated valueOf(String) method that translates a constant’s name into the constant itself. If you override the toString method in an enum type, consider writing a fromString method to translate the custom string representation back to the corresponding enum. The following code (with the type name changed appropriately) will do the trick for any enum, so long as each constant has a unique string representation:
枚举类型有自动的方法valueOf(String),这个方法将常量的名字转化为常量本身。如果重写了toString方法,尽量要写一个fromString方法(由于像上面代码一样,toString返回的是一个field,需要一个对应的从field方法fromString)
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;}},
IVIDE("/"){public double apply(double x, double y){return x / y;}};
public abstract double apply(double x, double 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));
}
}
note that attempting to have each constant put itself into a map from its own constructor does not work. It would cause a compilation error, which is good thing because if it were legal, it would cause a NullPointerException at runtime. Enum constructors aren’t permitted to access the enum’s static fields, with the exception of constant variables (Item 34). This restriction is necessary because static fields have not yet been initialized when enum constructors run. A special case of this restriction is that enum constants cannot access one another from their constructors.
Also note that the fromString method returns an Optional<String>. This allows the method to indicate that the string that was passed in does not represent a valid operation, and it forces the client to confront this possibility (Item 55).
注意,如果想要尝试在构造方法中将自己put到map中是不会生效的。会导致一个编译时错误,这是一个好事,因为这样是不合法的,如果允许会导致一个运行时空指针异常。枚举构造函数不允许访问枚举的静态字段,除了常量变量之外。这个限制是有必要的,因为静态字段在枚举构造函数运行时还没有完成初始化。一个具体的例子就是枚举常量不能在他们的构造函数中相互访问。
同时应该看到返回的是一个Optional<Operation>,这允许方法告诉暗示,传入的String不一定是一个合法的选项,并强制客户端接受不合法的可能。Optional是Java 8引入的,表示一个范型容器,保存一个范型值,或者仅保存null。
public enum MarkdownFileTag {
h1("#"), h2("##"), h3("###"), h4("####"), B("**"), I("*"), U("_"), li("* ");
private String tag;
MarkdownFileTag(String tag) {
this.tag = tag;
}
}
以上代码为枚举类代码,通过javap反编译后
effective_java_3_chapter_6_1.png
可以看到每个枚举常量都是一个静态的字段。如果枚举类中存在抽象方法,则在编译时采用静态内部类的形式生成代码。
package com.ryouji.enums;
public enum MarkdownFileTag {
h1("#"){
@Override
public void fun() {
System.out.println();
}
};
private String tag;
MarkdownFileTag(String tag) {
this.tag = tag;
}
public abstract void fun();
}
effective_java_3_chapter_6_2.png
A disadvantage of constant-specific method implementations is that they make it harder to share code among enum constants.
指定常量方法又个缺点就是很难共享代码,比如7个枚举类,其中5个的对于制定方法的实现相同,需要重复写相同的模版代码。
bad case
// Enum that switches on its value to share code - questionable
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
switch(this) {
case SATURDAY: case SUNDAY: // Weekend
overtimePay = basePay / 2;
break;
default: // Weekday
overtimePay = minutesWorked <= MINS_PER_SHIFT ?
0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
This code is undeniably concise, but it is dangerous from a maintenance perspective. Suppose you add an element to the enum, perhaps a special value to represent a vacation day, but forget to add a corresponding case to the switch statement. The program will still compile, but the pay method will silently pay the worker the same amount for a vacation day as for an ordinary weekday.
以上代码很简洁,但是从维护的角度看是很危险的。假设你添加一个元素到枚举中,可能是一个新的节日类型,但是忘记添加了对应的switch的case,代码依然可以编译通过,但是pay方法会按照普通的周末加班费结算节假日加班费。
What you really want is to be forced to choose an overtime pay strategy each time you add an enum constant. Luckily, there is a nice way to achieve this. The idea is to move the overtime pay computation into a private nested enum, and to pass an instance of this strategy enum to the constructor for the PayrollDay enum. The PayrollDay enum then delegates the overtime pay calculation to the strategy enum, eliminating the need for a switch statement or constant-specific method implementation in PayrollDay. While this pattern is less concise than the switch statement, it is safer and more flexible:
你真正想要的是每次增加一个枚举常量时,强制其选择一种加班费策略。幸运的是,这有一个很好的方式来实现。这个主意是将加班费计算移动到一个内嵌私有的枚举中,然后将这个策略枚举的实例作为构造方法的参数传到PayrollDay枚举中。然后可以通过策略枚举作为代理的方式调用加班费算法,PayrollDay枚举久不需要switch语句或者特定于常量的枚举实现了(每个枚举实现抽象方法的方式)。这种设计方法相比于switch更佳简洁,安全和灵活。
// The strategy enum pattern
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
PayrollDay() { this(PayType.WEEKDAY); } // Default
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
}, WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
} };
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
If switch statements on enums are not a good choice for implementing constant-specific behavior on enums, what are they good for? Switches on enums are good for augmenting enum types with constant-specific behavior. For example, suppose the Operation enum is not under your control and you wish it had an instance method to return the inverse of each operation.
You should also use this technique on enum types that are under your control if a method simply doesn’t belong in the enum type. The method may be required for some use but is not generally useful enough to merit inclusion in the enum type.
switch语句不是和给枚举常量增加特定行为,switch语句更适合给外部的枚举增加一些基于外部枚举类型的常量行为。(就是说不舍和在枚举类的内部用switch,可以在调用枚举类的时候使用switch,主要是由于,枚举类型增加,不会影响既有逻辑。如果内部使用,在新增枚举时,如果忘记添加枚举常量对应的switch方法,会造成使用默认逻辑的情况,可能是错误的。如果外部使用,在使用时,枚举类型是满足业务需求的,新增加枚举类型是,如果原代码处没有需求,是不需要修改的。)
这个技术同样适用于,这个枚举类在你的控制下,但是这个简单的方法不属于这个枚举类,但是这个方法会被使用,但是不足以有用到添加在枚举类中。
Enums are, generally speaking, comparable in performance to int constants. A minor performance disadvantage of enums is that there is a space and time cost to load and initialize enum types, but it is unlikely to be noticeable in practice.
枚举相比于整型常量,经常谈论的就是性能问题。枚举在性能上确实有一些小的不足,因为枚举需要存储空间大一些,需要消耗时间去加载,去初始化,但是在实践中是不需要过多考虑的。
So when should you use enums? Use enums any time you need a set of constants whose members are known at compile time. Of course, this includes “natural enumerated types,” such as the planets, the days of the week, and the chess pieces. But it also includes other sets for which you know all the possible values at compile time, such as choices on a menu, operation codes, and command line flags. It is not necessary that the set of constants in an enum type stay fixed for all time. The enum feature was specifically designed to allow for binary compatible evolution of enum types.
什么时候使用枚举,在任何你需要一组常量,并且常量的值在编译的时候就知道的情况。包括自然枚举类型,像行星,一周的天数,国际象棋的棋子。同时包括,在编译期你知道所有可能值的情况。比如菜单的选项,操作码,命令行标志。枚举类型的常量集合没有必要一直保持不变。枚举的特性是为二进制兼容演变专门设计的。(最后一句的意思应该是相比于其他语言的枚举类型说的.// 竟然是oracle官网上的关于java8枚举类说明里面的!!!)
Summary
In summary, the advantages of enum types over int constants are compelling. Enums are more readable, safer, and more powerful. Many enums require no explicit constructors or members, but others benefit from associating data with each constant and providing methods whose behavior is affected by this data. Fewer enums benefit from associating multiple behaviors with a single method. In this relatively rare case, prefer constant-specific methods to enums that switch on their own values. Consider the strategy enum pattern if some, but not all, enum constants share common behaviors.
总的来说,枚举类相比于int常量的优点是不言而喻的。枚举类型更佳易读,安全,功能强大。许多枚举不需要显式构造函数或成员,但是,在每个枚举常量有不同的成员数据,而每个枚举常量的行为又受到成员数据影响的时候^[1] ,我们将会受益于,有显示构造方法和成员变量枚举类系。少数的枚举受益于将多个行为使用一个方法(方法签名相同,行为不同),这种情况^[2] 下请使用constant-specific方法,而不是使用枚举的switch语句。在很多枚举使用相同行为的时候^[3] (不是所用枚举的行为都相同),请使用策略模式。
^[1]
// Enum type with data and behavior
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
^[2]
// Enum type with constant-specific method implementations
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;}},
IVIDE{public double apply(double x, double y){return x / y;}};
public abstract double apply(double x, double y);
}
^[3]
// The strategy enum pattern
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
PayrollDay() { this(PayType.WEEKDAY); } // Default
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
private enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
}, WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
} };
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
Item 35: Use instance fields instead of ordinals
用实数域代替序数
// Abuse of ordinal to derive an associated value - DON'T DO THIS
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() {
return ordinal() + 1;
}
}
While this enum works, it is a maintenance nightmare. If the constants are reordered, the numberOfMusicians method will break.
可以有效实现,但是维护困难。如果枚举常量进行了重排序,numberOfMusicians方法会被破坏。(ordinal是基于枚举类的顺序的)
Luckily, there is a simple solution to these problems. Never derive a value associated with an enum from its ordinal; store it in an instance field instead:
幸运的是,对于这个问题,有一个简单的解决方法。永远不要关联一个枚举值和他的ordinal,可以使用一个实例字段存储来替代。
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) {
this.numberOfMusicians = size;
}
public int numberOfMusicians() {
return numberOfMusicians;
}
}
Summary
The Enum specification has this to say about ordinal: “Most programmers will have no use for this method. It is designed for use by general-purpose enum-based data structures such as EnumSet and EnumMap.” Unless you are writing code with this character, you are best off avoiding the ordinal method entirely.
枚举规范关于序数说到:“大多数程序员不会使用到这个方法(ordinal())。这个方法是设计用来支持基于枚举的通用数据结构的,像EnumSet,EnumMap”。除非你写基于枚举的数据结构,否则最好避免使用ordinal方法
Item 36:Use EnumSet instead of bit fields
使用EnumSet替代位域
The java.util package provides the EnumSet class to efficiently represent sets of values drawn from a single enum type. This class implements the Set interface, providing all of the richness, type safety, and interoperability you get with any other Set implementation. But internally, each EnumSet is represented as a bit vector. If the underlying enum type has sixty-four or fewer elements and most do the entire EnumSet is represented with a single long, so its performance is comparable to that of a bit field. Bulk operations, such as removeAll and retainAll, are implemented using bitwise arithmetic, just as you’d do manually for bit fields. But you are insulated from the ugliness and error-proneness of manual bit twiddling: the EnumSet does the hard work for you.
java.util包提供了EnumSet类来高效的表示一个枚举类型的多个类的集合。该类实现了Set接口,提供了丰富操作,线程安全的特性,并且和其他Set实现可以互通使用。在内部,每一个EnumSet的元素表示一个比特位。在EnumSet下拥有64或者更少的元素(大多数情况如此),EnumSet的实现是用一个long来表示。因此性能上可以域位域相比。大量的操作,像removeAll和retainAll都使用位操作算法,就像你在管理位域。同时又可以避免易出错的,丑陋的代码,因为EnumSet完成了这个艰巨的任务。
Summary
In summary, just because an enumerated type will be used in sets, there is no reason to represent it with bit fields. The EnumSet class combines the conciseness and performance of bit fields with all the many advantages of enum types described in Item 34. The one real disadvantage of EnumSet is that it is not, as of Java 9, possible to create an immutable EnumSet, but this will likely be remedied in an upcoming release. In the meantime, you can wrap an EnumSet with Collections.unmodifiableSet, but conciseness and performance will suffer.
总的来说,正因为枚举类型要用在sets中,没有原因使用位域来做。EnumSet类集合了位域简洁性,性能位的优点和Item 34中枚举类型的优点于一身。枚举类型唯一的缺点是,直到Java 9,他都没办法创建一个不可变EnumSet,但是这一点可能在以后的发行版中修复。可以使用Collections.unmodifiableSet封装EnumSet,但是这会降低简洁性和性能。
Item 37: Use EnumMap instead of ordinal indexing
使用EnumMap替代序数索引
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for this key, the old
* value is replaced.
*
* @param key the key with which the specified value is to be associated
* @param value the value to be associated with the specified key
*
* @return the previous value associated with specified key, or
* <tt>null</tt> if there was no mapping for key. (A <tt>null</tt>
* return can also indicate that the map previously associated
* <tt>null</tt> with the specified key.)
* @throws NullPointerException if the specified key is null
*/
public V put(K key, V value) {
typeCheck(key);
int index = key.ordinal();
Object oldValue = vals[index];
vals[index] = maskNull(value);
if (oldValue == null)
size++;
return unmaskNull(oldValue);
}
Summary
In summary, it is rarely appropriate to use ordinals to index into arrays: use EnumMap instead. If the relationship you are representing is multidimensional, use EnumMap<..., EnumMap<...>>. This is a special case of the general principle that application programmers should rarely, if ever, use Enum.ordinal (Item 35).
总的来说,很少使用序数索引在数组中,使用EnumMap替代他。如果你在表达多维的关系,使用EnumMap<..., EnumMap<...>>。应用程序的程序员在一板情况下都不会使用ordinal,即使要用也很少。
Item 38: Emulate extensible enums with interfaces
用接口模拟可伸缩的枚举
In almost all respects, enum types are superior to the typesafe enum pattern described in the first edition of this book [Bloch01]. On the face of it, one exception concerns extensibility, which was possible under the original pattern but is not supported by the language construct. In other words, using the pattern, it was possible to have one enumerated type extend another; using the language feature, it is not. This is no accident. For the most part, extensibility of enums turns out to be a bad idea. It is confusing that elements of an extension type are instances of the base type and not vice versa. There is no good way to enumerate over all of the elements of a base type and its extensions. Finally, extensibility would complicate many aspects of the design and implementation.
几乎所有方面来看,枚举类型都是优于第一版中的类型安全枚举模式的。在考虑扩展性的方面有一个特例,在原来的接口常量的模式下是可能实现的,但是在枚举方法下,得不到语言结构的支持的。(说的是枚举不支持继承)换句话说,使用以前的模式是可以实现枚举类型继承其他类型的。使用语言特性的时候就不行。这不是偶然。大多数情况下,扩展枚举类型是一个坏主意。令人困惑的是,扩展类型的元素是基类型的实例,反之亦然。 枚举基本类型及其扩展的所有元素没有好的方法。 最后,可扩展性会使设计和实现的很多方面复杂化。
Luckily, there is a nice way to achieve this effect using enum types. The basic idea is to take advantage of the fact that enum types can implement arbitrary inter- faces by defining an interface for the opcode type and an enum that is the standard implementation of the interface. For example, here is an extensible version of the Operation type from Item 34:
幸运的是,使用枚举类型有一个很好的方法来实现这种效果。基本思想是利用枚举类型可以通过为opcode类型定义一个接口,并实现任意接口。例如,这里是来自条目 34的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;
}
}
How to use
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(ExtendedOperation.class, x, y);
}
private static <T extends Enum<T> & Operation> void test(
Class<T> opEnumType, double x, double y) {
for (Operation op : opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private static void test(Collection<? extends Operation> opSet,
double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
The resulting code is a bit less complex, and the test method is a bit more flexible: it allows the caller to combine operations from multiple implementation types. On the other hand, you forgo the ability to use EnumSet (Item 36) and EnumMap (Item 37) on the specified operations.
以上两个代码看起来不那么复杂,test方法也很灵活,它允许调用者结合operations的多个实现。另一方面它也放弃了使用EnumSet和EnumMap的途径(以上两个类的Key必须是枚举类型)
A minor disadvantage of the use of interfaces to emulate extensible enums is that implementations cannot be inherited from one enum type to another. If the implementation code does not rely on any state, it can be placed in the interface, using default implementations (Item 20). In the case of our Operation example, the logic to store and retrieve the symbol associated with an operation must be duplicated in BasicOperation and ExtendedOperation. In this case it doesn’t matter because very little code is duplicated. If there were a larger amount of shared functionality, you could encapsulate it in a helper class or a static helper method to eliminate the code duplication.
使用接口来模拟可扩展枚举的一个小缺点是,实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现(条目 20)将其放置在接口中。在我们的Operation示例中,存储和检索与操作关联的符号的逻辑必须在BasicOperation和ExtendedOperation中重复。在这种情况下,这并不重要,因为很少的代码是冗余的。如果有更多的共享功能,可以将其封装在辅助类或静态辅助方法中,以消除代码冗余。
Summary
In summary, while you cannot write an extensible enum type, you can emulate it by writing an interface to accompany a basic enum type that implements the interface. This allows clients to write their own enums (or other types) that implement the interface. Instances of these types can then be used wherever instances of the basic enum type can be used, assuming APIs are written in terms of the interface.
总之,虽然不能编写可扩展的枚举类型,但是你可以编写一个接口来配合实现接口的基本的枚举类型,来对它进行模拟。这允许客户端编写自己的枚举(或其它类型)来实现接口。如果API是根据接口编写的,那么在任何使用基本枚举类型实例的地方,都可以使用这些枚举类型实例。
Item 39: prefer annotations to naming patterns
注解优先于命名模式
命名模式:使用约定的命名规则,类系AOP的表达式。
Historically, it was common to use naming patterns to indicate that some program elements demanded special treatment by a tool or framework. For example, prior to release 4, the JUnit testing framework required its users to designate test methods by beginning their names with the characters test [Beck04]. This technique works, but it has several big disadvantages. First, typographical errors result in silent failures. For example, suppose you accidentally named a test method tsetSafetyOverride instead of testSafetyOverride. JUnit 3 wouldn’t complain, but it wouldn’t execute the test either, leading to a false sense of security.
A second disadvantage of naming patterns is that there is no way to ensure that they are used only on appropriate program elements.
A third disadvantage of naming patterns is that they provide no good way to associate parameter values with program elements.
历史上,通常使用命名模式来指示一些程序元素需要呗工具或者框架特殊处理。比如,在JUnit 4之前,JUnit需要用户使用test开头来命名需要测试的方法。这种方法是有效的,但是有很多缺点。首先,排版错误会导致悄无声息的失败。比如,错将testSafetyOverride写成tsetSafetyOverride。Junit 3 不会发出警告,但是他不会执行test方法,给人一种安全的错觉。
命名模式的第二个缺点是:无法保证只有特定的方法遵守则个规则。(无法保证别的方法名无意中遵守这个规则而被调用)比如,只想测试一个类中的一个方法,但是其他方法也有test开头的。
第三个缺点是:没办法将参数值和程序元素关联起来。(断言值不方便传递)比如:测试时候的成功条件是方法抛出制定异常。如果使用命名模式会导致方法名过长,编译器也无法校验。
// Marker annotation type declaration
import java.lang.annotation.*;
/**
* Indicates that the annotated method is a test method.
* Use only on parameterless static methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
The comment before the Test annotation declaration says, “Use only on parameterless static methods.” It would be nice if the compiler could enforce this, but it can’t, unless you write an annotation processor to do so. For more on this topic, see the documentation for javax.annotation.processing.
在Test注解之前声明说“只能使用在无参静态方法上”。如果编译器能强制这样是好的,但是并不能(没有原注解来限制注解是不是只能用在什么样的方法上),除非你写一个注解处理器(在编译期执行的,不是通过反射在运行期执行的)取做这件事。可以查看javax.annotation.processing的文档获取更多相关信息。
处理方式
// Program to process marker annotations
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " failed: " + exc);
} catch (Exception exc) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
可以使用如下代码实现,代多个参数的注解
// Annotation type with an array parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
对应的处理方法
if(m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Exception>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Exception> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
As of Java 8, there is another way to do multivalued annotations. Instead of declaring an annotation type with an array parameter, you can annotate the declaration of an annotation with the @Repeatable meta-annotation, to indicate that the annotation may be applied repeatedly to a single element. This meta-annotation takes a single parameter, which is the class object of a containing annotation type, whose sole parameter is an array of the annotation type [JLS, 9.6.3]. Here’s how the annotation declarations look if we take this approach with our ExceptionTest annotation. Note that the containing annotation type must be annotated with an appropriate retention policy and target, or the declarations won’t compile:
在JAVA 8 中,有一种新的方法来处理多注解值的情况。取代原来在注解中使用参数数组,可以使用@Repeatable元注解,来表示这个注解可能要在一个元素上使用多次。这个元注解需要一个参数,这个参数是包含被这个@Repeatable注解标注的注解的类型,这个参数类型对应的注解仅有的参数是被@Repeatable注解标注的注解的数组。以下是怎么使用这个注解在ExceptionTest上。注意@Repeatable注解参数的注解类型必须是有一定规则的,否则会编译不通过。
// Repeatable annotation type
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Exception> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// Code containing a repeated annotation
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() { ... }
Processing repeatable annotations requires care. A repeated annotation generates a synthetic annotation of the containing annotation type. The getAnnotationsByType method glosses over this fact, and can be used to access both repeated and non-repeated annotations of a repeatable annotation type. But isAnnotationPresent makes it explicit that repeated annotations are not of the annotation type, but of the containing annotation type. If an element has a repeated annotation of some type and you use the isAnnotationPresent method to check if the element has an annotation of that type, you’ll find that it does not. Using this method to check for the presence of an annotation type will therefore cause your program to silently ignore repeated annotations. Similarly, using this method to check for the containing annotation type will cause the program to silently ignore non-repeated annotations. To detect repeated and non-repeated annotations with isAnnotationPresent, you much check for both the annotation type and its containing annotation type. Here’s how the relevant part of our RunTests program looks when modified to use the repeatable version of the ExceptionTest annotation:
在处理可重复注解时需要注意。重复的注解会生成一个包含重复注解的综合注解。getAnnotationsByType方法隐藏了这个事实,这个方法既可以访问重复注解也可以访问包含重复注解的非重复注解。但是isAnnotationPresent方法使重复注解(指被注解包含的注解,即ExceptionTest)的注解类型不是注解的实际类型,包含重复注解的类型才是实际类型。如果一个元素有一些重复注解类型的注解,使用isAnnotationPresent方法检查这个元素是否好友那个类型的注解的时候,会发现,注解不是重复注解的类型。使用这个方法检查是否存在一个类型的注解,会悄无声息的忽略重复注解。类似的,使用这个方法检查可重复注解的容器,则会忽略不可重复注解。为了使用isAnnotationPresent方法发现可重复和不可重复注解,你必须检查可重复注解容器类型和他包含的可重复注解类型。以下是当我们使用可重复注解时的,处理程序:
// Processing repeatable annotations
if(m.isAnnotationPresent(ExceptionTest.class)
||m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests =
m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("Test %s failed: %s %n", m, exc);
}
}
}
多个重复注解才会被封装为注解容器
Item 40: Consistently use the Override annotation
坚持使用Override注解
Luckily, the compiler can help you find this error, but only if you help it by telling it that you intend to override Object.equals.
使用@Override注解可以告诉编译期你在重写方法,编译器可以帮你保证重写的方法是正确的形式。
Therefore, you should use the Override annotation on every method declaration that you believe to override a superclass declaration. There is one minor exception to this rule. If you are writing a class that is not labeled abstract and you believe that it overrides an abstract method in its superclass, you needn’t bother putting the Override annotation on that method. In a class that is not declared abstract, the compiler will emit an error message if you fail to override an abstract superclass method. However, you might wish to draw attention to all of the methods in your class that override superclass methods, in which case you should feel free to annotate these methods too. Most IDEs can be set to insert Override annotations automatically when you elect to override a method.
然而,应该在确认重写(父类有实现,抽象方法不算)父类的方法时,保证使用@Override。父类的方法是抽象的时候,不需要@Override注解标注方法。因为一个非抽象类,如果没有重写父类的抽象方法编译器会报错。然而,你可能希望去关注你重写父类方法的方法,这种时候,也应该随时在这些方法上加注解。大多数IDE可以自动的插入Override注解,当你选择重写一个方法的时候。
Summary
In summary, the compiler can protect you from a great many errors if you use the Override annotation on every method declaration that you believe to override a supertype declaration, with one exception. In concrete classes, you need not annotate methods that you believe to override abstract method declarations (though it is not harmful to do so).
总的来说,编译期可以保护你少犯很多错误,如果你在任何你觉得会覆盖超类方法上加@override注解(除了覆盖抽象类的抽象方法)时。不用使用@Override,在覆盖抽象类的抽象方法时。
Item 41: Use marker interface to define types
使用标记接口定义类
You may hear it said that marker annotations (Item 39) make marker interfaces obsolete. This assertion is incorrect. Marker interfaces have two advantages over marker annotations. First and foremost, marker interfaces define a type that is implemented by instances of the marked class; marker annotations do not. The existence of a marker interface type allows you to catch errors at compile time that you couldn’t catch until runtime if you used a marker annotation.
你可能担心,说:标记注解已经足够了,表姐接口应该淘汰了。这种断言是不对的,标记接口相比标记注解有两个优点,第一个,也是最主要的是:标记接口定义一个被被标记类实现的类型;标记注解就不行。标记接口可以使你在编译期捕获一些问题,而这些问题如果使用标记注解的话可能要到运行时才被发现。
Had the argument of this method been of type Serializable, an attempt to serialize an inappropriate object would have been detected at compile time (by type checking). Compile-time error detection is the intent of marker inter- faces, but unfortunately, the ObjectOutputStream.write API does not take advantage of the Serializable interface: its argument is declared to be of type Object, so attempts to serialize an unserializable object won’t fail until runtime.
由于ObjectOutputStream.write方法的入参是Object类型,导致该方法没有实现标记接口关于错误可以在编译期发现的优点。
Another advantage of marker interfaces over marker annotations is that they can be targeted more precisely. If an annotation type is declared with target ElementType.TYPE, it can be applied to any class or interface. Suppose you have a marker that is applicable only to implementations of a particular interface. If you define it as a marker interface, you can have it extend the sole interface to which it is applicable, guaranteeing that all marked types are also subtypes of the sole interface to which it is applicable.
标记接口的另一个优点是可以更佳精确的定位目标。如果一个注解的Target是ElementType.TYPE,这个注解可以使用在类、接口、枚举、注解上,如果在你只想标记一个特殊接口时,使用标记接口,使你的接口继承标记接口就可以了,而如果你使用的是标记注解,这个注解可能被使用在类上。(只想在接口,但是可以在类上,叫不精确)
The chief advantage of marker annotations over marker interfaces is that they are part of the larger annotation facility. Therefore, marker annotations allow for consistency in annotation-based frameworks.
标记注解的主要优点是标记注解是更广泛的注解机制的一部分,可以作为编程元素之一在基于注解的框架中广泛使用。
So when should you use a marker annotation and when should you use a marker interface? Clearly you must use an annotation if the marker applies to any program element other than a class or interface, because only classes and interfaces can be made to implement or extend an interface. If the marker applies only to classes and interfaces, ask yourself the question “Might I want to write one or more methods that accept only objects that have this marking?” If so, you should use a marker interface in preference to an annotation. This will make it possible for you to use the interface as a parameter type for the methods in question, which will result in the benefit of compile-time type checking. If you can convince yourself that you’ll never want to write a method that accepts only objects with the marking, then you’re probably better off using a marker annotation. If, additionally, the marking is part of a framework that makes heavy use of annotations, then a marker annotation is the clear choice.
什么时候使用注解,什么时候使用接口呢?如果标记的域涉及程序的所有元素,不只限于类和接口的话,使用注解,因为只有类和接口可以实现或继承接口。如果标记的域只限于接口或者类,需要考虑“我是否要写一些只处理这种类型对象的方法”,如果是就使用接口。(接口可以作为范型参数传递)这可以使你能用接口作为参数类型传递,这样可以使用编译期类型检查。如果你能说服自己,永远不写一个只接收这个对象类型的参数的方法,这个时候可以考虑使用标记注解。如果,这个标记是大量使用注解的框架的一部分,就使用标记注解。
Summary
In summary, marker interfaces and marker annotations both have their uses. If you want to define a type that does not have any new methods associated with it, a marker interface is the way to go. If you want to mark program elements other than classes and interfaces or to fit the marker into a framework that already makes heavy use of annotation types, then a marker annotation is the correct choice. If you find yourself writing a marker annotation type whose target is ElementType.TYPE, take the time to figure out whether it really should be an annotation type or whether a marker interface would be more appropriate.
In a sense, this item is the inverse of Item 22, which says, “If you don’t want to define a type, don’t use an interface.” To a first approximation, this item says, “If you do want to define a type, do use an interface.”
总的来说,标记接口和标记注解都有其作用。如果,你想定一个没有任何新方法的类型,标记接口是一个很好的选择。如果,你想标记除类和接口之外的程序元素,或者为了使标记点适应已经广泛使用标记注解的框架,使用一个标记注解是一个不错的选择。如果你发现你正在写一个类、接口、枚举、注解的标记注解,应该考虑下是不是标记接口更加合适。
直觉上,这章和Item 22:如果不是想定义一个类型,不要用接口,是相反的,本章最接近的意思是,如果想定义一个类型,就使用接口。
网友评论