Effective Java 读书笔记(五):枚举和注解
2017-10-10 20:17
399 查看
Effective Java 读书笔记五枚举和注解
用 enum 代替 int 常量
用实例域代替序数
用 EnumSet 代替位域
使用 EnumMap 代替序数索引
用接口模拟可伸缩的枚举
注解优先于命名模式
坚持使用 Override 注解
用标记接口定义类型
在编程语言中还没有枚举类型之前,表示枚举类型的常用模式是声明一组 int 常量,每个类型成员一个常量:
这种模式被称为 int 枚举模式,有诸多不足:
1. 类型安全性:以上的 APPLE 和 ORANGE 本来是不同类型,然而都用 int 表示,那么如果将 APPLE 传到了需要 ORANGE 的地方,编译器也不会发现问题。
2. 由于没有命名空间,每个枚举值都需要用前缀 APPLE_ 或 ORANGE_ 来区分。
3. 将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。
4. int 枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的 int 值发生了变化,那么客户端也需要重新编译。
从 Java1.5 开始,出现一个特殊的枚举类型:
枚举类型 enum 是实例受控的,是单例的泛型化,同时也是类型安全的。同时,其默认的 toString 方法输出的是枚举常量名字符串,比如 FUJI、PIPPIN。
Java 中的枚举本质上是 int 值,可以使用 ordinal() 获取,默认从 0 开始依次递增。
调整枚举常量的顺序后,并不需要重新编译客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中。比如下面的这个类,被反编译后我们发现字节码中存储的是枚举类型和常量名,而不是 int 值:
Java 中的枚举 enum 也可以拥有 field、方法、抽象方法,还可以覆盖 toString 方法:
考虑用一个枚举表示薪资包中的工作天数,这个枚举有一个方法,根据给定工人的基本工资以及当天工作时间,计算当天报酬。在 5 个工作日中,超过 8 小时会产生加班工资,而在周末则所有工作都产生加班工资。解决这一问题的比较好的方案是使用策略枚举模式:将加班工资的计算移到了私有的嵌套枚举中,将某一天适用的策略枚举传递到 PayrollDay 的构造器中。PayrollDay 中不再需要 switch 语句或特定于常量的方法实现,这种模式虽然没有 switch 语句简洁,但是更加安全(减少增加枚举时遗漏增加逻辑的可能性),也更加灵活。
ordinal 一般是用于像 EnumSet、EnumMap 这样的通用数据结构的。在程序中,不要去调用 ordinal 方法,如果需要这样的一个 int 值,请在实例域中维护。
EnumMap 运行速度能够与序数索引的数组相媲美,因为其内部实现也是序数索引的数组。但是,使用 EnumMap 更不容易出错(类型安全,隐藏了实现细节,替你干了脏活、累活)。
虽然枚举类型不是可扩展的,但是接口类型是可扩展的。在使用任何基础操作的地方,都可以使用新的枚举,只要 API 是被写成采用接口类型,而非实现。
泛型 < T extends Enum< T> & IOperation> 表示 T 既是枚举,又是 IOperation 的子类型。
1. 拼写错误后难以发现。
2. 无法确保他们只用于相应的程序元素上,容易被多种框架误用。
3. 没有提供将参数值与程序元素关联起来的好方法,方法名所能携带的信息太少。
注解能够很好的解决以上的所有问题。比如 Junit 中标记测试方法的 Test 注解:
注解只是提供信息给程序使用,并不会改变被注解代码的语义。
注解也是一种标记,那么它和标记接口有什么区别呢?标记注解只提供说明信息,无法定义类型;而标记接口是一种类型,由被标记类的实例实现,能够在编译时捕捉使用注解在运行时才能捕捉到的一些错误。另一方面,标记接口可以继承别的接口,而注解不能。
标记注解相对于标记接口的一大优点是,可以给已被使用的注解类型添加更多的信息,可以被用到类、方法、域上,适用范围更广。
简单来说,他们的区别就是注解和接口的区别。
用 enum 代替 int 常量
用实例域代替序数
用 EnumSet 代替位域
使用 EnumMap 代替序数索引
用接口模拟可伸缩的枚举
注解优先于命名模式
坚持使用 Override 注解
用标记接口定义类型
Effective Java 读书笔记(五):枚举和注解
用 enum 代替 int 常量
枚举类型 enum type 是指由一组固定常量组成合法值的类型,比如一年中的季节、太阳系中的行星。在编程语言中还没有枚举类型之前,表示枚举类型的常用模式是声明一组 int 常量,每个类型成员一个常量:
public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2;
这种模式被称为 int 枚举模式,有诸多不足:
1. 类型安全性:以上的 APPLE 和 ORANGE 本来是不同类型,然而都用 int 表示,那么如果将 APPLE 传到了需要 ORANGE 的地方,编译器也不会发现问题。
2. 由于没有命名空间,每个枚举值都需要用前缀 APPLE_ 或 ORANGE_ 来区分。
3. 将 int 枚举常量翻译成可打印的字符串,并没有很便利的方法。
4. int 枚举是编译时常量,被编译到使用它们的客户端中。如果与枚举常量关联的 int 值发生了变化,那么客户端也需要重新编译。
从 Java1.5 开始,出现一个特殊的枚举类型:
public enum Apple { FUJI, PIPPIN, GRANNY; } public enum Orange { NAVEL, TEMPLE, BLOOD; }
枚举类型 enum 是实例受控的,是单例的泛型化,同时也是类型安全的。同时,其默认的 toString 方法输出的是枚举常量名字符串,比如 FUJI、PIPPIN。
Java 中的枚举本质上是 int 值,可以使用 ordinal() 获取,默认从 0 开始依次递增。
调整枚举常量的顺序后,并不需要重新编译客户端代码,因为导出常量的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中。比如下面的这个类,被反编译后我们发现字节码中存储的是枚举类型和常量名,而不是 int 值:
public class TestEnum2 { public static void main(String[] args) { System.out.println(Apple.FUJI); } }
Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = Fieldref #23.#24 // com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #3 // Field com/qunar/flight/tts/afare/utils/Apple.FUJI:Lcom/qunar/flight/tts/afare/utils/Apple; 6: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 9: return
Java 中的枚举 enum 也可以拥有 field、方法、抽象方法,还可以覆盖 toString 方法:
public enum Operation { PLUS("+") { @Override double apply(double a, double b) { return a + b; } }, MINUS("-") { @Override double apply(double a, double b) { return a - b; } }, TIMES("*") { @Override double apply(double a, double b) { return a + b; } }, DIVIDE("/") { @Override double apply(double a, double b) { return a + b; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } // 覆盖 toString,输出 symbol @Override public String toString() { return symbol; } // 将 symbol 转换成 Operation public Operation fromString(String symbol) { for (Operation operation : values()) { if (operation.symbol.equals(symbol)) { return operation; } } return null; } abstract double apply(double a, double b); }
考虑用一个枚举表示薪资包中的工作天数,这个枚举有一个方法,根据给定工人的基本工资以及当天工作时间,计算当天报酬。在 5 个工作日中,超过 8 小时会产生加班工资,而在周末则所有工作都产生加班工资。解决这一问题的比较好的方案是使用策略枚举模式:将加班工资的计算移到了私有的嵌套枚举中,将某一天适用的策略枚举传递到 PayrollDay 的构造器中。PayrollDay 中不再需要 switch 语句或特定于常量的方法实现,这种模式虽然没有 switch 语句简洁,但是更加安全(减少增加枚举时遗漏增加逻辑的可能性),也更加灵活。
public enum PayrollDay { 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; PayrollDay(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; } }; 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); } } }
用实例域代替序数
所以枚举都有一个 ordinal 方法,返回每个枚举常量在类型中的数字位置,其值与枚举常量的顺序相关,完全由编译器定义。ordinal 一般是用于像 EnumSet、EnumMap 这样的通用数据结构的。在程序中,不要去调用 ordinal 方法,如果需要这样的一个 int 值,请在实例域中维护。
用 EnumSet 代替位域
位域表示法允许利用位操作,有效地执行像 union 并集、intersection 交集这样的集合操作。位域有着 int 枚举常量所有的缺点,可以使用 EnumSet 代替,EnumSet 就是用单个 long 表示,很多操作都是利用位算法来实现,其性能和位域相当。public static final int STYLE_BOLD = 1 << 0; public static final int STYLE_ITALIC = 1 << 1; public static final int STYLE_UNDERLINE = 1 << 2; // 使用例子 int STYLE_BOLD_ITALIC = STYLE_BOLD | STYLE_ITALIC;
// RegularEnumSet 是 EnumSet 的一个实现 class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> { /** * Bit vector representation of this set. The 2^k bit indicates the * presence of universe[k] in this set. */ private long elements = 0L; // add 利用位操作实现 public boolean add(E e) { typeCheck(e); long oldElements = elements; elements |= (1L << ((Enum<?>)e).ordinal()); return elements != oldElements; } }
使用 EnumMap 代替序数索引
最好不要用序数 ordinal 来索引数组,而要使用 EnumMap。如果你所表达的关系是多维的,就是用 EnumMap< …, EnumMap< …>>。EnumMap 运行速度能够与序数索引的数组相媲美,因为其内部实现也是序数索引的数组。但是,使用 EnumMap 更不容易出错(类型安全,隐藏了实现细节,替你干了脏活、累活)。
public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V> implements java.io.Serializable, Cloneable { private final Class<K> keyType; private transient K[] keyUniverse; private transient Object[] vals; 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); } }
用接口模拟可伸缩的枚举
枚举类是 final 的,不可被继承扩展的。如果想要可伸缩的枚举,可以使用接口来模拟:public interface IOperation { double apply(double a, double b); } public enum Operation implements IOperation { PLUS("+") { @Override public double apply(double a, double b) { return a + b; } }, MINUS("-") { @Override public double apply(double a, double b) { return a - b; } }, TIMES("*") { @Override public double apply(double a, double b) { return a + b; } }, DIVIDE("/") { @Override public double apply(double a, double b) { return a + b; } }; private final String symbol; Operation(String symbol) { this.symbol = symbol; } // 覆盖 toString,输出 symbol @Override public String toString() { return symbol; } }
虽然枚举类型不是可扩展的,但是接口类型是可扩展的。在使用任何基础操作的地方,都可以使用新的枚举,只要 API 是被写成采用接口类型,而非实现。
泛型 < T extends Enum< T> & IOperation> 表示 T 既是枚举,又是 IOperation 的子类型。
注解优先于命名模式
在 Java1.5 之前,经常使用命名模式 naming pattern 表明有些程序元素需要通过某种工具或框架进行特殊处理。例如,Junit 测试框架原本要求用户一定要用 test 作为测试方法名称的开头。这种方法可行,但是有很多缺点:1. 拼写错误后难以发现。
2. 无法确保他们只用于相应的程序元素上,容易被多种框架误用。
3. 没有提供将参数值与程序元素关联起来的好方法,方法名所能携带的信息太少。
注解能够很好的解决以上的所有问题。比如 Junit 中标记测试方法的 Test 注解:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface Test { static class None extends Throwable { private static final long serialVersionUID= 1L; private None() { } } // 期望抛出的异常 Class<? extends Throwable> expected() default None.class; // 方法执行超时时间,0 表示不设置超时时间 long timeout() default 0L; }
注解只是提供信息给程序使用,并不会改变被注解代码的语义。
坚持使用 Override 注解
Override 注解只能用在方法声明中,表示被注解的方法覆盖率超类中的一个方法声明。坚持使用 Override 注解能够让编译器帮你检查大量的错误。用标记接口定义类型
标记接口 marker interface 是没有包含方法声明的接口,而只是指明一个类实现了具有某种属性的接口,比如 Serializable 接口。注解也是一种标记,那么它和标记接口有什么区别呢?标记注解只提供说明信息,无法定义类型;而标记接口是一种类型,由被标记类的实例实现,能够在编译时捕捉使用注解在运行时才能捕捉到的一些错误。另一方面,标记接口可以继承别的接口,而注解不能。
标记注解相对于标记接口的一大优点是,可以给已被使用的注解类型添加更多的信息,可以被用到类、方法、域上,适用范围更广。
简单来说,他们的区别就是注解和接口的区别。
相关文章推荐
- effective java 读书笔记---第六章 枚举与注解
- 【读书笔记】《Effective Java》(5)--枚举和注解
- [Effective Java 读书笔记] 第6章 枚举和注解
- 《Effective Java》读书笔记五(枚举和注解)
- 《Effective Java》第6章 枚举和注解
- Effective java -- 5 枚举和注解
- [Effective Java]第六章 枚举和注解
- 读书笔记--编写高质量代码 改善java程序的151个建议(六)枚举与注解
- 《Effective Java》——学习笔记(泛型&枚举和注解)
- 《 Effective Java》关于泛型,方法和枚举,注解的建议
- 《Effective java》读书笔记5——枚举
- 《Effective Java》第6章 枚举与注解
- effective java(枚举和注解)
- [Effective Java]第六章 枚举和注解
- Java- 装箱、枚举、注解
- android用注解代替枚举
- 枚举、自动装箱与注解(元数据)
- [Effective Java 读书笔记] 第三章类和接口 第二十-二十一条
- java高级特性之 枚举,注解,可变行参