枚举和注解
Table of Contents
用enum代替int常量
int枚举模式
public static final int APP_START=1; public static final int APP_PAUSE=0; public static final int APP_STOP =2; public static final int PLAY_START=1; public static final int PLAY_PAUSE=0; public static final int PLAY_STOP =2;
- 类型安全:如果将AAP_传到需要PLAY_的方法中,编译器不会出现警告,甚至允许int i =(APP_START - PLAY_START) / PLAY_START
- 使用方便:如果修改一个int常量的值,会引起客户端使用代码的重新编译。把int常量打印出来得到的只是数字,很难和真正的变量名关联起来
如果用的是String常量,这样的变体被称作String枚举模式。虽然可以解决打印字符串的问题,但是由于需要经常比较字符串,所以性能糟糕。更严重的是客户端代码往往会把这样的常量硬编码到代码里面,而一旦字符串常量发生变化,会导致客户端逻辑出错
枚举
通过共有的静态final域为每个枚举常量导出实例的类。枚举是单例的泛型化,本质上是单元素的枚举
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } public enum Orange { NAVEL, TEMPLE, BLOOD }
枚举的优势
- 枚举类型拥有自己的命名空间,可以允许同名常量
- 增加和重排枚举类型中常量,而无需重新编译客户端(新增常量自然无法使用)
- toString可以获取常量的字面值
- 提供了所有的Object的方法的高级实现,实现了Comparable和Serializable接口,并针对枚举类型的可任意改变性设计了序列化方式
- 枚举可任意添加方法和成员
枚举模式
添加成员和方法
为九大行星添加重量和半径常量成员,并提供方法来使用这些数据计算重力
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; private 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 static void main(String[] args) { double earthWeight = Double.parseDouble("175"); double mass = earthWeight / Planet.EARTH.surfaceGravity(); //Weight on MERCURY is 66.133672 //Weight on VENUS is 158.383926 //Weight on EARTH is 175.000000 //Weight on MARS is 66.430699 //Weight on JUPITER is 442.693902 //Weight on SATURN is 186.464970 //Weight on URANUS is 158.349709 //Weight on NEPTUNE is 198.846116 for (Planet p : Planet.values()) { System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass)); } }
设计枚举和设计类的原则是相同的:可见性最低,可变性最小
- 所有的成员都应该是final private
- 如果可能就不提供public的方法
- 如果需要被外部使用才做成顶层类
添加抽象方法
提供一个方法来执行四大基本计算常量所表示的算术运算
public enum UglyOperation { // Enum type that switches on its own value - questionable PLUS, MINUS, TIMES, DIVIDE; // Do the arithmetic op represented by this constant 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); } }
- 没有throw语句就不能进行编译,然而实际上不可能执行到这里
- 增加了新的枚举常量,却忘记给switch增加相应的条件,仍然可以编译,但在试图运用新的运算的时候就会运行失败
在枚举类中声明一个抽象方法,这样在添加新的常量时候就必须实现这个方法
public enum Operation { PLUS("+") { @Override double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override double apply(double x, double y) { return x / y; } }; private final String symbol; abstract double apply(double x, double y); private Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
覆盖的toString方法也使得客户端代码输出更漂亮
public static void main(String[] args) { String[] arg = {"2", "4"}; //2.000000 + 4.000000 = 6.000000 //2.000000 - 4.000000 = -2.000000 //2.000000 * 4.000000 = 8.000000 //2.000000 / 4.000000 = 0.500000 double x = Double.parseDouble(arg[0]); double y = Double.parseDouble(arg[1]); for (Operation op : Operation.values()) { System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); } }
fromString
枚举类型有一个自动产生的valueOf方法,将常量的字面值转成对应的枚举
如果在枚举类型中覆盖toString,需要考虑编写一个formString方法,将toString输出的字符串转回相应的枚举
// Implementing a fromString method on an enum type private static final Map<String, Operation> stringToEnum = new HashMap<String, Operation>(); static { // Initialize map from constant name to enum constant for (Operation op : values()) stringToEnum.put(op.toString(), op); } // Returns Operation for string, or null if string is invalid public static Operation fromString(String symbol) { return stringToEnum.get(symbol); }
策略枚举
根据某工人的基本工资以及当天的工作时间,来计算它当天的薪酬。在5个工作日中,超过正常8小时的工作时间都会产生加班工资;在双休日中,所有工作都产生加班工资
public enum DangerPayrollDay { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT = 8; double pay(double hourseWorked, double payRate) { double basePay = hourseWorked * payRate; double overtimePay; switch (this) { case SATURDAY: case SUNDAY: overtimePay = hourseWorked * payRate / 2; break; default: overtimePay = hourseWorked <= HOURS_PER_SHIFT ? 0 : (hourseWorked - HOURS_PER_SHIFT) * payRate / 2; break; } return basePay + overtimePay; } }
这段代码很简单,但是非常脆弱。假设将一个元素添加到枚举中,如一个特殊的假期(国庆节等),但忘了给switch语句添加相应的case,就会计算出错
为每一天实现计算工资方法
public enum UglyPayrollDay { MONDAY() { @Override double overtimePay(double hoursWorked, double payRate) { return weekdayPay(hoursWorked, payRate); } }, TUESDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekdayPay(hoursWorked, payRate); } }, WEDNESDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekdayPay(hoursWorked, payRate); } }, THURSDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekdayPay(hoursWorked, payRate); } }, FRIDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekdayPay(hoursWorked, payRate); } }, SATURDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekendPay(hoursWorked, payRate); } }, SUNDAY { @Override double overtimePay(double hoursWorked, double payRate) { return weekendPay(hoursWorked, payRate); } }; private static final int HOURS_PER_SHIFT = 8;//正常工作时数 //抽象出加班工资计算 abstract double overtimePay(double hoursWorked, double payRate); //计算工资 double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate;//公用 return basePay + overtimePay(hoursWorked, payRate); } //双休日加班工资算法 double weekendPay(double hoursWorked, double payRate) { return hoursWorked * payRate / 2; } //正常工作日加班工资 double weekdayPay(double hoursWorked, double payRate) { return hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2; } }
这段代码很难看,有很多常量实现pay方法都是重复的,而且每增加新的一天,就可能新的额外重复代码
添加一个私有的枚举类型,用它作为计算工资的策略属性。计算加班工资的方法被实现在这个策略枚举内,在原来的枚举类型每次添加新天常量,只需要声明使用哪种策略类型
public enum StrategyPayrollDay { 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 StrategyPayrollDay(PayType payType) { this.payType = payType; } double pay(double hoursWorked, double payRate) { return payType.pay(hoursWorked, payRate); } // The strategy enum type private enum PayType { WEEKDAY { double overtimePay(double hours, double payRate) { return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2; } }, WEEKEND { double overtimePay(double hours, double payRate) { return hours * payRate / 2; } }; private static final int HOURS_PER_SHIFT = 8; abstract double overtimePay(double hrs, double payRate); double pay(double hoursWorked, double payRate) { double basePay = hoursWorked * payRate; return basePay + overtimePay(hoursWorked, payRate); } } }
使用枚举进行switch
如果想要在枚举外面的类中为不同的枚举常量增加不同的行为,可以把枚举用在switch语句中
private static Operation inverse(Operation op) { switch (op) { case PLUS: return Operation.MINNUS; case MINNUS: return Operation.PLUS; case TIMES: return Operation.DIVIDE; case DIVIDE: return Operation.TIMES; default: throw new AssertionError("Unkown op: " + op); } }
用实例中的成员代替序数
使用ordinal()方法能够获得实例在枚举的顺序,从0开始
// Abuse of ordinal to derive an associated value - DON'T DO THIS public enum Ensemble { DUET, TRIO, QUARTET, QUINTET, SEPTET, OCTET, NONET, DECTET; public int numberOfMusicians() { return ordinal() + 1; } }
如果把变量重新排序,就可能导致numberOfMusicians方法出错
解决方法:为枚举常量添加int成员
// Enum with integer data stored in an instance field 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; private Ensemble(int size) { this.numberOfMusicians = size; } public int numberOfMusicians() { return numberOfMusicians; } }
大多数情况不应该调用ordinal方法,这个方法是为了给特殊的类如EnumSet,EnumMap而提供
用EnumSet代替位域
当常量被用于位运算的时候,通常这些常量会被声明为int常量,并且赋值成2的n次幂
// 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);
使用EnumSet代替int常量
// EnumSet - a modern replacement for bit fields public class Text { public static 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(Text.Style.BOLD, Text.Style.ITALIC));
用EnumMap代替序数索引
public class Herb { public enum Type { ANNUAL, PERENNIAL, BIENNIAL } private final String name; private final Type type; public Herb(String name, Type type) { this.name = name; this.type = type; } @Override public String toString() { return name; } }
使用enum的ordinal值作为数组的下标
// Using ordinal() to index an array - DON'T DO THIS! Herb[] garden = {new Herb("Basil", Type.ANNUAL), new Herb("Scallion", Type.PERENNIAL), new Herb("Dill", Type.BIENNIAL)}; // Indexed by Herb.Type.ordinal() Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; for (int i = 0; i < herbsByType.length; i++) { herbsByType[i] = new HashSet<>(); } for (Herb h : garden) { herbsByType[h.type.ordinal()].add(h); } //ANNUAL: [Basil] //PERENNIAL: [Scallion] //BIENNIAL: [Dill] for (int i = 0; i < herbsByType.length; i++) { System.out.printf("%s: %s%n", Herb.Type.values()[i], herbsByType[i]); }
使用int作为数组的下标是很危险的,没有枚举类型安全检查,一旦使用错误的int数值,运行时候就会有IndexoutOfArrayException。其次数组和范型并不兼容,不追求效率的情况下应该避免使用数组
解决方法:使用enumMap代替Set<Herb>[]
// Using an EnumMap to associate data with an enum Herb[] garden = {new Herb("Basil", Type.ANNUAL), new Herb("Scallion", Type.PERENNIAL), new Herb("Dill", Type.BIENNIAL)}; Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<>(Herb.Type.class); for (Herb.Type t : Herb.Type.values()) { herbsByType.put(t, new HashSet<>()); } for (Herb h : garden) { herbsByType.get(h.type).add(h); } System.out.println(herbsByType);
嵌套enumMap
更复杂的情况可能会用到嵌套的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); final Phase src; final Phase dst; Transition(Phase src, Phase dst) { this.src = src; this.dst = dst; } // Initialize the phase transition map private static final Map<Phase, Map<Phase, Transition>> m = new EnumMap<>(Phase.class); static { for (Phase p : Phase.values()) { m.put(p, new EnumMap<>(Phase.class)); } for (Transition trans : Transition.values()) { m.get(trans.src).put(trans.dst, trans); } } public static Transition from(Phase src, Phase dst) { return m.get(src).get(dst); } } }
总之不要调用ordinal方法
用接口模拟可扩展枚举
大多数情况下可扩展的枚举并不是一个好的设计
- 很难区分哪些枚举常量是父类型,哪些枚举常量是子类型
- 无法很好地对所有父类型和子类型的常量做枚举
- 扩展通常会使设计和使用变得更复杂
Java无法编写可扩展的枚举类,但是某些特定情况下需要扩展,比如使用定义的枚举常量的用户需要增加自己的枚举常量
- 声明枚举常量要实现的接口
public interface Operation { double apply(double x, double y); }
- 提供基本的枚举常量给客户端使用
// Emulated extensible enum using an interface public enum BasicOperation implements IOperation { PLUS("+") { @Override public double apply(double x, double y) { return x + y; } }, MINUS("-") { @Override public double apply(double x, double y) { return x - y; } }, TIMES("*") { @Override public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { @Override 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; } }
- 客户端通过实现接口的方式提供扩展的枚举,基本枚举和扩展枚举都可以统一作为IOperation的实现传递
// Emulated extension enum public enum ExtendedOperation implements IOperation { EXP("^") { @Override public double apply(double x, double y) { return Math.pow(x, y); } }, REMAINDER("%") { @Override public double apply(double x, double y) { return x % y; } }; private final String symbol; private ExtendedOperation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } }
传递整个扩展枚举类型
- 通过范型<T extends Enum<T> & IOperation>
private static <T extends Enum<T> & IOperation> void test( Class<T> opSet, double x, double y) { for (IOperation op : opSet.getEnumConstants()) { System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); } } public static void main(String[] args) { double x = 5.0; double y = 3.0; //5.000000 ^ 3.000000 = 125.000000 //5.000000 % 3.000000 = 2.000000 test(ExtendedOperation.class, x, y); }
- 使用Collection<? extends Operation>
private static void test(Collection<? extends IOperation> opSet, double x, double y) { opSet.forEach((op) -> { System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); }); } public static void main(String[] args) { double x = 5.0; double y = 3.0; //5.000000 ^ 3.000000 = 125.000000 //5.000000 % 3.000000 = 2.000000 test(Arrays.asList(ExtendedOperation.values()), x, y); }
使用接口模拟可扩展的枚举的缺点是有部分重复代码,如果重复代码太多,可以考虑抽取工具类
注解优先于命名模式
Java 1.5之前,一般使用命名模式表明有些程序元素需要通过某种工具或者框架进行特殊处理。例如,JUnit测试框架原本要求用户一定要用test作为测试方法名称的开头。
命名模式的缺点
- 无法处理命名失误的情况
- 无法确保它们只用于响应的程序元素上:比如某个类也以testXXX命名
- 没有提供将参数值与程序元素关联起来的好方法:比如要测试捕捉到某个特定Exception才算成功
注解
- @Retention(RetentionPolicy.RUNTIME): 表明注解在运行时保留(CLASS:编译器保留,运行时删除,SOURCE:源代码保留,编译器删除)
- @Target(ElementType.METHOD):表明注解作用于方法(还能作用与类型,成员,构造器,方法参数,局部变量,包,注解类型等)
/** * Indicates that the annotated method is a test method. Use only on * parameterless static methods. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { }
使用@Test的测试用例代码,如果拼错Test或者将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() { throw new RuntimeException("Boom"); //test should fail } 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() { } }
测试工具类
public class RunTests { public static void main(String[] args) throws Exception { int tests = 0; int passed = 0; //Class testClass = Class.forName(args[0]); Class testClass = Class.forName("klose.effj.annotation.Sample"); for (Method m : testClass.getDeclaredMethods()) { //通过反射获取@Test的方法 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); } }
测试结果
public static void klose.effj.annotation.Sample.m3() failed: java.lang.RuntimeException: Boom INVALID @Test: public void klose.effj.annotation.Sample.m5() public static void klose.effj.annotation.Sample.m7() failed: java.lang.RuntimeException: Crash Passed: 1, Failed: 3
有参数的注解
只有在抛出特殊异常才成功的注解
// Annotation type with a parameter @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { //特定异常类型 Class<? extends Exception> value(); }
测试用例
// Program containing annotations with a parameter public class Sample2 { @ExceptionTest(ArithmeticException.class) // Test should pass public static void m1() { int i = 0; i = i / i; } @ExceptionTest(ArithmeticException.class) // Should fail (wrong exception) public static void m2() { int[] a = new int[0]; int i = a[1]; } @ExceptionTest(ArithmeticException.class) // Should fail (no exception) public static void m3() { } }
测试工具类
public class RunExceptionTests { public static void main(String[] args) throws Exception { int tests = 0; int passed = 0; //Class testClass = Class.forName(args[0]); Class testClass = Class.forName("klose.effj.annotation.Sample2"); 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 wrappedEx) { Throwable exc = wrappedEx.getCause(); //获取注解中的value值,也就是想要捕捉的异常的class类型,以此与实际捕捉异常的Throwable做比较 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 public static void klose.effj.annotation.Sample2.m2() failed: expected java.lang.ArithmeticException, got java.lang.ArrayIndexOutOfBoundsException: 1 Test public static void klose.effj.annotation.Sample2.m3() failed: no exception Passed: 1, Failed: 2
数组作为参数的注解
捕捉多个异常中任意一个
// Annotation type with an array parameter @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionsTest { Class<? extends Exception>[] value(); }
测试用例
public class SampleWithExceptions { // Code containing an annotation with an array parameter @ExceptionsTest({IndexOutOfBoundsException.class, NullPointerException.class}) public static void doublyBad() { List<String> list = new ArrayList<String>(); // The spec permits this method to throw either // IndexOutOfBoundsException or NullPointerException list.addAll(5, null); } }
测试工具类
public class RunExceptionsTest { public static void main(String[] args) throws Exception { int tests = 0; int passed = 0; //Class testClass = Class.forName(args[0]); Class testClass = Class.forName("klose.effj.annotation.SampleWithExceptions"); for (Method m : testClass.getDeclaredMethods()) { if (m.isAnnotationPresent(ExceptionsTest.class)) { tests++; try { m.invoke(null); System.out.printf("Test %s failed: no exception%n", m); } catch (Throwable wrappedExc) { Throwable exc = wrappedExc.getCause(); Class<? extends Exception>[] excTypes = m.getAnnotation(ExceptionsTest.class).value(); int oldPassed = passed; 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); } } } } System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed); } }
测试结果
Passed: 1, Failed: 0
总是使用Override注解
只有写工具类的时候才有机会编写注解,但所有人都应该使用JDK提供的注解,其中@Override就是一个典型用法
用标记接口定义类型
标记接口:不包含任何方法的接口,如果某个类implements标记接口,往往只是表明某个类具有某种属性,比如Serializable
标记接口的优点
- 标记接口定义的类型是由被标记类的实例实现的,标记注解则没有定义这样的类型。这使得标记接口可以在编译时报错,而标记注解只有在运行时报错
- 使用标记接口的方法能够更加精确的对实现它的类型进行锁定:实现标记接口的只能是类
标记注解的优点
- 更方便给已被使用的注解类型添加更多的信息
- 不但适用类型,更适用方法,成员,包,构造器,方法参数等。。。