Effective Java 5
---恢复内容开始---
Item 34 使用枚举代替 int常量
1、使用枚举类型代替int枚举模式。
2、Int枚举模式是常变量,很难转换成好的打印字符,没有可靠的方法迭代其中的常量以及获取size.
3、枚举类型:
1 public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } 2 public enum Orange { NAVEL, TEMPLE, BLOOD }
4、枚举类型通过一个public static final 域输出其中每一个枚举常量。
5、枚举类型是final,没有可访问的构造器,也就是说是单例。
6、枚举类型提供了编译器的类型安全。
7、可以删除,添加或者重排序枚举元素,而不需要重新编译。
8、有着很好的toString方法。
9、枚举类型可以添加任意方法和域可以实现任意接口。
10、实现了 Comparable和 Serializable接口。
11、例子:
1 // Enum type with data and behavior 2 public enum Planet { 3 MERCURY(3.302e+23, 2.439e6), 4 VENUS (4.869e+24, 6.052e6), 5 EARTH (5.975e+24, 6.378e6), 6 MARS (6.419e+23, 3.393e6), 7 JUPITER(1.899e+27, 7.149e7), 8 SATURN (5.685e+26, 6.027e7), 9 URANUS (8.683e+25, 2.556e7), 10 NEPTUNE(1.024e+26, 2.477e7); 11 private final double mass; // In kilograms 12 private final double radius; // In meters 13 private final double surfaceGravity; // In m / s^2 14 // Universal gravitational constant in m^3 / kg s^2 15 private static final double G = 6.67300E-11; 16 // Constructor 17 Planet(double mass, double radius) { 18 this.mass = mass; 19 this.radius = radius; 20 surfaceGravity = G * mass / (radius * radius); 21 } 22 public double mass() { return mass; } 23 public double radius() { return radius; } 24 public double surfaceGravity() { return surfaceGravity; } 25 public double surfaceWeight(double mass) { 26 return mass * surfaceGravity; // F = ma 27 } 28 }
12、所有域自然是final的,不过最好使它private并提供public访问器。
13、调用例子:
1 public class WeightTable { 2 public static void main(String[] args) { 3 double earthWeight = Double.parseDouble(args[0]); 4 double mass = earthWeight / Planet.EARTH.surfaceGravity(); 5 for (Planet p : Planet.values()) 6 System.out.printf("Weight on %s is %f%n",p, p.surfaceWeight(mass)); 7 } 8 }
14、.value()方法,按声明的顺序返回包含所有枚举元素的数组。
15、结果:
1 Weight on MERCURY is 69.912739 2 Weight on VENUS is 167.434436 3 Weight on EARTH is 185.000000 4 Weight on MARS is 70.226739 5 Weight on JUPITER is 467.990696 6 Weight on SATURN is 197.120111 7 Weight on URANUS is 167.398264 8 Weight on NEPTUNE is 210.208751
16、删除一个元素不会影响剩余的元素,但是如果再进行重新编译,会提示一个有用的错误。
17、尽可能降低枚举方法的访问权限。
18、如果对每个枚举元素想要有不同的方法:
1 // Enum type that switches on its own value - questionable 2 public enum Operation { 3 PLUS, MINUS, TIMES, DIVIDE; 4 // Do the arithmetic operation represented by this constant 5 public double apply(double x, double y) { 6 switch(this) { 7 case PLUS: return x + y; 8 case MINUS: return x - y; 9 case TIMES: return x * y; 10 case DIVIDE: return x / y; 11 } 12 throw new AssertionError("Unknown op: " + this); 13 } 14 }
19、一个更好的方法是 在枚举类型中 声明一个抽象类。然后在每一个枚举元素的常量具体类体(constant-specific class body) 中重写它。这些方法叫做常量具体方法实现(constant-specific method implementation):
1 // Enum type with constant-specific method implementations 2 public enum Operation { 3 PLUS {public double apply(double x, double y){return x + y;}}, 4 MINUS {public double apply(double x, double y){return x - y;}}, 5 TIMES {public double apply(double x, double y){return x * y;}}, 6 DIVIDE{public double apply(double x, double y){return x / y;}}; 7 public abstract double apply(double x, double y); 8 }
20、与之前数据结合的例子:
1 // Enum type with constant-specific class bodies and data 2 public enum Operation { 3 PLUS("+") {public double apply(double x, double y) { return x + y; }}, 4 MINUS("-") {public double apply(double x, double y) { return x - y;}}, 5 TIMES("*") {public double apply(double x, double y) { return x * y;}}, 6 DIVIDE("/") {public double apply(double x, double y) { return x / y;}}; 7 private final String symbol; 8 Operation(String symbol) { this.symbol = symbol; } 9 @Override public String toString() { return symbol; } 10 public abstract double apply(double x, double y); 11 }
1 public static void main(String[] args) { 2 double x = Double.parseDouble(args[0]); 3 double y = Double.parseDouble(args[1]); 4 for (Operation op : Operation.values()) 5 System.out.printf("%f %s %f = %f%n",x, op, y, op.apply(x, y)); 6 }
21、枚举类型有个 valueof(String)方法,将常量的名字转换成常量本身。
22、如果复写了toString方法,考虑写一个fromString方法把String形式的表示转化为相应的枚举元素。例子:
// 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)); }
23、在Java8之前 使用对于hash map的迭代代替value()将值插入。但是想要通过每个元素的构造器进行插入是不行的。
24、枚举构造器不允许访问静态域,除了常变量。因为在构造器执行的时候静态域还未被初始化。同样也不能通过构造器访问另一个枚举常量。
25、常量具体方法实现(constant-specific method implementation)的一个缺点是使枚举常量共用代码困难,并且在添加新的枚举元素时容易出错。例子:
// 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; } }
26、好的方法,使用内部枚举类型:
// 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); } } }
27、Switch方法用在当你不能控制枚举类时增加枚举类型的常量具体行为(constant-specific behavior)。
1 // Switch on an enum to simulate a missing method 2 public static Operation inverse(Operation op) { 3 switch(op) { 4 case PLUS: return Operation.MINUS; 5 case MINUS: return Operation.PLUS; 6 case TIMES: return Operation.DIVIDE; 7 case DIVIDE: return Operation.TIMES; 8 default: throw new AssertionError("Unknown op: " + op); 9 } 10 }
28、在需要一系列常值并且在编译期就知道它的成员时,以及自然应该是枚举的时候(如一周7天等)使用枚举类型。
29、如第7条所说,枚举类型中的元素并不必须保持固定。
Item 35 使用实例域代替ordinals
1、所有枚举类型有一个ordinal方法,用于返回枚举元素在枚举类型中的位置(int)。这对于进行添加或删除元素来说时非常糟糕的。
1 // Abuse of ordinal to derive an associated value - DON'T DO THIS 2 public enum Ensemble { 3 SOLO, DUET, TRIO, QUARTET, QUINTET,SEXTET, SEPTET, OCTET, NONET, DECTET; 4 public int numberOfMusicians() { return ordinal() + 1; } 5 }
2、永远不要通过ordinal 获得一个和枚举元素有关的值。应该作为一个实例域进行储存:
1 public enum Ensemble { 2 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); 3 private final int numberOfMusicians; 4 Ensemble(int size) { this.numberOfMusicians = size; } 5 public int numberOfMusicians() { return numberOfMusicians; } 6 }
3、ordinal()方法只被设计用于基于枚举类型的数据结构 例如EnumSet EnumMap。
Item 36 使用EnumSet代替 位(bit)域
1、使用int 枚举模式在bit域上有这比普通的域上更多的缺点,比如需要确定最大长度,无法迭代等:
1 // Bit field enumeration constants - OBSOLETE! 2 public class Text { 3 public static final int STYLE_BOLD = 1 << 0; // 1 4 public static final int STYLE_ITALIC = 1 << 1; // 2 5 public static final int STYLE_UNDERLINE = 1 << 2; // 4 6 public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8 7 // Parameter is bitwise OR of zero or more STYLE_ constants 8 public void applyStyles(int styles) { ... } 9 }
2、一个好的例子使用EnumSet,它实现了Set接口因此可以使用所有Set的操作,并且类型安全。在内部每个EnumSet代表一个位向量:
1 public class Text { 2 public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } 3 // Any Set could be passed in, but EnumSet is clearly best 4 public void applyStyles(Set<Style> styles) { ... } 5 }
1 text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
3、唯一的缺点是无法创建一个不变的EnumSet,但是可以使用Collections.unmodifiableSet进行包装 虽然会有性能损失。
Item 37 使用EnumMap 代替 序数索引(ordinal indexing))
例子:
1 class Plant { 2 enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL } 3 final String name; 4 final LifeCycle lifeCycle; 5 Plant(String name, LifeCycle lifeCycle) { 6 this.name = name; 7 this.lifeCycle = lifeCycle; 8 } 9 @Override public String toString() { 10 return name; 11 } 12 }
1、一个错误的使用例子:
1 // Using ordinal() to index into an array - DON'T DO THIS! 2 Set<Plant>[] plantsByLifeCycle =(Set<Plant>[]) new Set[Plant.LifeCycle.values().length]; 3 for (int i = 0; i < plantsByLifeCycle.length; i++) 4 plantsByLifeCycle[i] = new HashSet<>(); 5 for (Plant p : garden) 6 plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); 7 // Print the results 8 for (int i = 0; i < plantsByLifeCycle.length; i++) { 9 System.out.printf("%s: %s%n",Plant.LifeCycle.values()[i], plantsByLifeCycle[i]); 10 }
首先的问题是使用了泛型数组使得无法获得类型安全的保证。更重要的问题是当使用由枚举叙述所索引的数组时,int的正确性必须自行保证。
2、正确的方式使用EnumMap:
1 // Using an EnumMap to associate data with an enum 2 Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =new EnumMap<>(Plant.LifeCycle.class); 3 for (Plant.LifeCycle lc : Plant.LifeCycle.values()) 4 plantsByLifeCycle.put(lc, new HashSet<>()); 5 for (Plant p : garden) 6 plantsByLifeCycle.get(p.lifeCycle).add(p); 7 System.out.println(plantsByLifeCycle);
注意构造器使用了Class对象作为key
可以使用流进行进一步的简化:
1 // Naive stream-based approach - unlikely to produce an EnumMap! 2 System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle)));
1 // Using a stream and an EnumMap to associate data with an enum 2 System.out.println(Arrays.stream(garden).collect(groupingBy(p -> p.lifeCycle,() -> new EnumMap<>(LifeCycle.class), toSet())));
3、一个错误的例子 使用两个枚举的ordinal 来代替一个二维数组的索引:
1 // Using ordinal() to index array of arrays - DON'T DO THIS! 2 public enum Phase { 3 SOLID, LIQUID, GAS; 4 public enum Transition { 5 MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; 6 / / Rows indexed by from-ordinal, cols by to-ordinal 7 private static final Transition[][] TRANSITIONS = {{ null, MELT, SUBLIME },{ FREEZE, null, BOIL },{ DEPOSIT, CONDENSE, null }}; 9 // Returns the phase transition from one phase to another 10 public static Transition from(Phase from, Phase to) { 11 return TRANSITIONS[from.ordinal()][to.ordinal()]; 12 } 13 } 14 }
这显然也是一个极其脆弱的结构。
4:正确的例子:
// 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 = 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); } } }
1 // Adding a new phase using the nested EnumMap implementation 2 public enum Phase { 3 SOLID, LIQUID, GAS, PLASMA; 4 public enum Transition { 5 MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS); 6 ... // Remainder unchanged 7 } 8 }
Item 38 使用接口扩展枚举
1、在语法层面上是无法扩展使一个枚举继承另一个枚举的,但是使用这个模式可以达到相同的效果。
2、至少有一种情况下需要用到,那就是操作码。
3、例子:
// 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; } }
4、可以把一个扩展了的枚举实例传递给,原本基础枚举作为参数的地方。并且扩展的枚举中所有添加的或者复写的元素都能使用。
5、调用:
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)); }
6、这里的Class 对象代表既是枚举又是Operation的子类。
Item 39使用注解代替命名模式(naming Patterns)
1、以前通过特定的命名模式来表明某些程序需要被工具或者框架特殊对待。这有很多缺点。首先拼写错误不易察觉,第二没有办法可以保证用在合适的程序元素中。第三没有好的办法将参数值和程序元素联系在一起。
2、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 { }
用来注解 注解本身 的注解叫做元注解 上面的@Rentention()表示应该在运行期被保留@Target()表示只能用在方法声明上。这个注解应该被只用在无参的静态方法上,如果不是将会使测试工具在运行期处理这个问题。
3、@Test注解使用例子:
// Program containing marker annotations public class Sample { @Test public static void m1() { } // Test should pass public static void m2() { } @Test public static void m3() { // Test should fail throw new RuntimeException("Boom"); } public static void m4() { } @Test public void m5() { } // INVALID USE: nonstatic method public static void m6() { } @Test public static void m7() { // Test should fail throw new RuntimeException("Crash"); } public static void m8() { } }
Test注解在语义上对类没有直接的影响,它只为感兴趣的程序提供信息例如:
// 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); } }
这里通过使用Method.invoke调用所有被Test注解了的方法。
如果测试方法抛出了一个异常,反射服务会把它包装成InvocationTargetException,这是使用getCause方法进行抽取的。
其他的异常会以Exception形式抛出表示对Test注解的错误使用。
结果:
public static void Sample.m3() failed: RuntimeException: Boom Invalid @Test: public void Sample.m5() public static void Sample.m7() failed: RuntimeException: Crash Passed: 1, Failed: 3
4、如果需要当抛出特定的异常表明测试成功时。可以使用ExceptionTest注解。
5、ExceptionTest注解:
// Annotation type with a parameter import java.lang.annotation.*; /** * Indicates that the annotated method is a test method that * must throw the designated exception to succeed. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable> value(); }
6、使用Classl对象作为ExceptionTest例子的参数,例子:
// Program containing annotations with a parameter public class Sample2 { @ExceptionTest(ArithmeticException.class) public static void m1() { // Test should pass int i = 0; i = i / i; } @ExceptionTest(ArithmeticException.class) public static void m2() { // Should fail (wrong exception) int[] a = new int[0]; int i = a[1]; } @ExceptionTest(ArithmeticException.class) public static void m3() { } // Should fail (no exception) }
if (m.isAnnotationPresent(ExceptionTest.class)) { tests++; try { m.invoke(null); System.out.printf("Test %s failed: no exception%n", m); } catch (InvocationTargetException wrappedEx) { Throwable exc = wrappedEx.getCause(); Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); if (excType.isInstance(exc)) { passed++; } else { System.out.printf("Test %s failed: expected %s, got %s%n",m, excType.getName(), exc); } } catch (Exception exc) { System.out.println("Invalid @Test: " + m); } }
7、如果需要多个异常形式可以使用数组:
// Annotation type with an array parameter @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Exception>[] value(); }
// Code containing an annotation with an array parameter @ExceptionTest({ IndexOutOfBoundsException.class,NullPointerException.class }) public static void doublyBad() { List<String> list = new ArrayList<>(); // The spec permits this method to throw either // IndexOutOfBoundsException or NullPointerException list.addAll(5, null); }
8、也可以使用Repeatable注解。
Item 40 不断使用Override 注解
1、这个注解只能被方法声明上,这个方法可以来自于接口也可以来自于类。
2、一个错误的例子:
// Can you spot the bug? public class Bigram { private final char first; private final char second;
public Bigram(char first, char second) { this.first = first; this.second = second; } public boolean equals(Bigram b) { return b.first == first && b.second == second; } public int hashCode() { return 31 * first + second; } public static void main(String[] args) { Set<Bigram> s = new HashSet<>(); for (int i = 0; i < 10; i++) for (char ch = 'a'; ch <= 'z'; ch++) s.add(new Bigram(ch, ch)); System.out.println(s.size()); } }
3、以上这个例子错误在 equals方法的参数类型并非Object,导致被没有复写这个方法,而是重载了这个方法。而调用的仍然是Object.equals导致传入其中的两个相同字母而hash值却还是不同的,因此仍然被放进了Set里。
4、正确的例子:
@Override public boolean equals(Object o) { if (!(o instanceof Bigram)) return false; Bigram b = (Bigram) o; return b.first == first && b.second == second; }
5、因此也应该使用Override注解在任何一个你相信它复写了超类声明对的方法声明上。
6、在一个抽象类或者接口中,值得去注解所有你认为复写了超类或超接口的方法。
7、一个例外是在一个实体类中,如果复写一个超类的抽象方法可以不需要添加Override注解
Item41 使用标记接口定义类型
1、标记接口是指不包含任何方法声明仅仅标记实现了这个接口的类会拥有某些特性的接口。
2、标记接口比起标记注解有两个优点。一是标记接口定义了一个类型并且标记类的实例所实现了,而标记注解没有。这可以使在编译期间通过类型检查检查到错误。二是目标可以更加精准,如果想要让一个标记只适用于某个特定接口的实现,可以把它作为一个标记接口,然后继承这个类。这样所有实用的实现都是这个接口的子类型,这样保证了类型安全。
3、标记注解的主要好处是作为整套注解体系而存在的。
4、如果想要标记除了类和接口以外的元素,应该使用注解。
5、如果标记应用于类和接口。试问自己,是否想要一个写一个或多个方法只接受拥有这个标记的对象作为参数呢?如果答案是肯定的应该使用标记接口。如果不是使用标记注解。