5.枚举和注解_EJ
第30条: 用enum代替int常量
枚举类型是指由一组固定的常量组成合法值得类型。例如一年中的季节,太阳系中的行星或一副牌中的花色。在开发中我们经常在类使用static final来定义一个int常量。这种方式存在诸多不足,在类型安全性和使用方便性方面没有任何帮助。
看几个枚举类型的例子,体会一下其用法。
太阳系中的行星:
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; private final double radius; private final double surfaceGravity; private static final double G = 6.67300E-11; 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; } }
public class WeightTable { public static void main(String[] args) { // TODO Auto-generated method stub double earthWeight = 70; double mass = earthWeight / Planet.EARTH.surfaceGravity(); for(Planet p : Planet.values()){ System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass)); } } }
一个操作符的例子:
public enum Operation { PLUS, MINUS, TIMES, DIVIDE; 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); } }
使用switch方式显然不够好,我们改进一下。
public enum Operation2 { PLUS("+") {double apply(double x, double y){return x + y;}}, MINUS("-") {double apply(double x, double y){return x - y;}}, TIMES("*") {double apply(double x, double y){return x * y;}}, DIVIDE("/") {double apply(double x, double y){return x / y;}}; private final String symbol; Operation2(String symbol){ this.symbol = symbol; } @Override public String toString() { return symbol; } private final static Map<String, Operation2> stringToEnum = new HashMap<String, Operation2>(); static { for(Operation2 op : Operation2.values()){ stringToEnum.put(op.toString(), op); } } public static Operation2 fromString(String symbol){ return stringToEnum.get(symbol); } abstract double apply(double x, double y); public static void main(String[] args){ double x = Double.parseDouble("2"); double y = Double.parseDouble("4"); for(Operation2 op : Operation2.values()){ System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); } Operation2 op = fromString("+"); System.out.println(op); } }
一个计算正常工资和加班工资的例子:
public enum PayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT = 8; double pay(double hoursWorked, double payRate){ double basePay = hoursWorked * payRate; double overtimePay; switch(this){ case SATURDAY: case SUNDAY: overtimePay = hoursWorked * payRate / 2; default: overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; break; } return basePay + overtimePay; } }
代码十分简洁,但从维护角度来看,它非常危险,同样是因为switch。如果新添加一个枚举,但忘记在switch语句中添加相应的case,则程序逻辑会出问题。
修改版:
public enum PayrollDay2 { MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND); private final PayType payType; private PayrollDay2(PayType payType) { this.payType = payType; } double pay(double hoursWorked, double payRate){ return payType.pay(hoursWorked, payRate); } private enum PayType{ WEEKDAY { @Override double overtimePay(double hrs, double payRate) { return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate / 2; } }, WEEKEND { @Override double overtimePay(double hrs, double payRate) { return hrs * payRate / 2; } }; abstract double overtimePay(double hrs, double payRate); private static final int HOURS_PER_SHIFT = 8; double pay(double hoursWorked, double payRate){ double basePay = hoursWorked * payRate; return basePay + overtimePay(hoursWorked, payRate); } } }
总而言之,与int相比,枚举类型的优势是不言而喻的。枚举易读得多,也更加安全,功能更加强大。
第31条:用实例域代替序数
枚举类型有一个ordinal方法,它范围该常量的序数从0开始,不建议使用这个方法,因为这不能很好地对枚举进行维护,正确应该是利用实例域,例如:
public enum Ensemble2 { SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8), NONET(9), DECTET(10); private final int nuberOfMusicians; Ensemble2(int size){ this.nuberOfMusicians = size; } public int numberOfMusicians(){ return nuberOfMusicians; } }
第32条:用EnumSet代替位域
位域表示法允许利用位操作,有效地执行先联合和交集这样的集合操作。但是位域有着int枚举常亮的所有缺点,甚至更多。当位域一数字形式打印时,翻译位域比翻译简单的int枚举常量要困难得多。甚至,要遍历位域表示的所有元素都没有很容易的方法。
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 << 3; //4 public static final int STYLE_STRIKETHROUGH = 1 << 3; //8 public void applyStyles(int styles){} }
java.util 包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合。这个类实现Set接口,提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体的实现上,每个EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素——大多数如此。整个EnumSet就用单个long来表示,因此它的性能比的上位域的性能。批处理,如removeAll和retainAll,都是利用位算法来实现的。就像手工替代位域实现得那样。但是可以避免手工操作时容易出现的错误以及不太雅观的代码,因为EnumSet替你完成了这项艰巨的工作。`
//EnumSet - a modern replacement for bit fields public class Text2 { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }; //Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { System.out.println(styles); } public void test() { applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC)); } }
总而言之,正因为枚举类型要用在集合Set中,所以没有理由用位域来表示它。
第33条:用EnumMap代替序数索引
有时候我们会利用ordinal()方法来索引数组的代码。这方法可行,但隐藏着问题,例如数组与泛型不能兼容,程序需要进行未受检的转换,序数int不能提供枚举类型的安全,容易使用错误的值等。举些例子就明白了:
public class Herb { //表示一年生、多年生或者两年生植物 public enum Type { ANNUAL, PERENNIAL, BIENNIAL } private final String name; private final Type type; Herb(String name, Type type){ this.name = name; this.type = type; } @Override public String toString() { return name; } public static void main(String[] args){ //Using ordinal() to index an array - Don't do this Herb[] garden = {new Herb("plant1", Type.ANNUAL), new Herb("plant2", Type.BIENNIAL), new Herb("plant3", Type.PERENNIAL), new Herb("plant4", Type.PERENNIAL), new Herb("plant5", Type.ANNUAL), new Herb("plant6", Type.BIENNIAL)}; // @SuppressWarnings("unchecked") // Set<Herb>[] herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length]; // for(int i = 0; i < herbsByType.length; i++){ // herbsByType[i] = new HashSet<Herb>(); // } // for(Herb h : garden){ // herbsByType[h.type.ordinal()].add(h); // } // for(int i = 0; i < herbsByType.length; i++){ // System.out.printf("%s : %s%n", Herb.Type.values()[i], herbsByType[i]); // } //Using an EnumMap to associate data with an enum Map<Herb.Type, Set<Herb>> herbsByType2 = new EnumMap<>(Herb.Type.class); for(Herb.Type t : Herb.Type.values()){ herbsByType2.put(t, new HashSet<Herb>()); } for(Herb h : garden){ herbsByType2.get(h.type).add(h); } System.out.println(herbsByType2); } }
public enum Phase2 { 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 Phase2 src; private final Phase2 dst; Transition(Phase2 src, Phase2 dst){ this.src = src; this.dst = dst; } private static final Map<Phase2, Map<Phase2, Transition>> m = new EnumMap<>(Phase2.class); static { for(Phase2 p : Phase2.values()){ m.put(p, new EnumMap<Phase2, Transition>(Phase2.class)); } for(Transition trans : Transition.values()){ m.get(trans.src).put(trans.dst, trans); } } public static Transition from(Phase2 src, Phase2 dst){ return m.get(src).get(dst); } } }
总之,最好不用序数来索引数组,而要用EnumMap。
第34条:用接口模拟可伸缩的枚举
之前写过一个operation的例子:
/** * 加减乘除枚举 * Created by yulinfeng on 8/20/17. */ public enum Operation { PLUS { double apply(double x, double y) { return x + y; } }, MIUS { double apply(double x, double y) { return x - y; } }, TIMES { double apply(double x, double y) { return x * y; } }, DEVIDE { double apply(double x, double y) { return x / y; } }; abstract double apply(double x, double y); }
这个方法也不错,但从软件开发的可扩展性来说这并不是一个好的方法,软件可扩展性并不是在原有代码上做修改,这个时候就需要接口出场了,修改上述例子:
public interface IOperation { double apply(double x, double y); }
public enum ExtendedOperation implements IOperation{ EXP("^"){ public double apply(double x, double y){ return Math.pow(x, y); } }; private final String symbol; private ExtendedOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } public static void main(String[] args){ double x = Double.parseDouble("2"); double y = Double.parseDouble("3"); } }
这样当我们需要扩展操作符枚举的时候只需要重新实现IOperation接口即可。这样就达到代码的可扩展性,但这样做的有一个小小的不足,就是无法从一个枚举类型继承到另外一个枚举类型。
第35条:注解优先于命名模式
使用命名模式来表明有些程序元素需要通过某种工具或者框架来进行特殊处理有几个严重的缺点:
1.文字拼写错误会导致失败,且没有任何提示
2.无法确保它们只用于相应的程序元素上
3.不能提供将参数值与程序元素关联起来的好方法。
举几个注解的例子:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { }
public class Sample { @Test public static void m1(){ System.out.println("aaaxxx"); } public static void m2(){} @Test public static void m3(){ throw new RuntimeException("Boom"); } @Test public void m4(){} @Test public static void m5(){ System.out.println("aaaxxx2"); } @Test public static void m6(){ throw new RuntimeException("Boom2"); } }
public class RunTests { public static void main(String[] args) throws Exception{ int tests = 0; int passed = 0; String className = "chapter6.Sample"; Class testClass = Class.forName(className); 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); } }
测试main方法通过反射技术来获取相关注解,并测试相关方法。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Exception> value(); }
public class Sample2 { @ExceptionTest(ArithmeticException.class) public static void m1(){ int i = 0; i = i / i; } @ExceptionTest(ArithmeticException.class) public static void m2(){ int[] a = new int[0]; int i = a[1]; } @ExceptionTest(ArithmeticException.class) public static void m4(){ } public static void main(String[] args) throws Exception{ int tests = 0; int passed = 0; String className = "chapter6.Sample2"; Class testClass = Class.forName(className); for(Method m : testClass.getDeclaredMethods()){ if(m.isAnnotationPresent(ExceptionTest.class)){ tests++; try{ m.invoke(null); System.out.printf("Test %s failed: no exception %n", m); }catch (InvocationTargetException wrappedExc){ Throwable exc = wrappedExc.getCause(); Class<? extends Exception> 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); } } } System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed); } }
上面这个例子类似于处理Test注解的代码,但点不同,这段代码提取了注解参数的值,并用它检验该测试抛出的异常是否为正确的类型。
总之,我们大多数程序员都不需要定义注解类型,但是我们应该使用平台提供的注解类型。
第36条:坚持使用override注解
对于传统程序员而言,在所有的注解类型中,最重要的就是override注解了。它表示被注解的方法声明覆盖了超类中的一个声明。如果坚持使用该注解,可以防止一大类的非法错误。举个双字母对的例子:
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; // } @Override public boolean equals(Object obj) { if(!(obj instanceof Bigram)){ return false; } Bigram b = (Bigram) obj; 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()); } }
从代码中可以看出,被注释掉的equals方法没有正确覆盖超类的该方法。而如果坚持使用override注解,则会进行代码检验,不会出现这种错误。
第37条:用标记接口定义类型
标记接口是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,例如实现了Serializable接口表示它可被实例化。在有的情况下使用标记注解比标记接口可能更好,但书中提到了两点标记接口胜过标记注解:
1) 标记接口定义的类型是由被标记类的实例实现的;标记注解则没有定义这样的类型。 //这条有点不好理解,希望有大神有更加通俗地解释
2) 尽管标记注解可以锁定类、接口、方法,但它是针对所有,标记接口则可以被更加精确地锁定。
另外书中也提到标记注解优于标记接口的地方:那就是能标记程序元素而非类和接口,且在未来能给标记添加更多的信息。