30.使用enum代替int常量
以前的方案
在枚举出现前,都是 使用常量的方式,如
public static final int APPLE_FUJI = 0;
public static final int ORANGE_NAVEL = 0;
这种方称为枚举常量
,其弊端有:
- 如果与枚举常量关联的
int
发生变化,则必须重新编译 - 如果将枚举常量翻译成可打印的字符串,只能见到一个数字,没有太大的用处.
枚举方式
java 中的枚举本质上是
int
值.
public enum Apple{FUJI}
public enum Apple{NAVEL}
- 枚举类基本想法 : 通过公有的
final
域为 每个枚举常量 导出实例的类. - 因为没有可访问的构造器, 因此枚举类型都是
final
的 - 因为
客户端
无法创建 枚举实例
,也不能对其进行扩展
,因此枚举是 实例受控的,单例的泛型化
- 编译时类型安全,声明后
取值
一定是 枚举中的有效值
之一 - 通过复写
toString
,可以将枚举
的值打印出来. - 枚举可以添加任意的
方法和域
,并实现任意的接口
枚举的高级用法
-
switch
,枚举中可以通过switch(this)
来根据不同的域
做不同的操作,
示例 : Operation.java
当然,上面种方式并不好,建议使用
特定于常量的方法实现
(onstant-specific method implementation) Operation1.java可以通过构造函数传递参数,例如,示例:Operation.java,
op
打印是调用toString
打印出了+,-,*,/
;利用
策略枚举
,可以用在更加安全,灵活的场景.如:书中的加班场景,每添加一种枚举常量就强制添加一种策略
,示例代码:PayrollDay.java
总结
-
switch
枚举 适合于给外部的
枚举类型增加特定于 常量
的行为. - 一般来说,枚举会 优先使用
comparable
类型.而非int
类型 - 需要一组固定常量的时候就可以使用枚举.
- 枚举
装载和初始化的时候会有 空间和时间 的成本.
31.用实例域代替序数
简介
枚举的
ordinal()
方法会返回枚举常量在类型中的数字位置,
但是尽量不要使用它,因为当重新排序
后,会对客户端造成破坏.
正确的做法是,将他保存在一个 实例域
中.
示例
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4);
private final int numberOf;
Ensemble(int _i) {
this.numberOf = _i;
}
public int getNumberOf() {
return numberOf;
}
}
32.使用enumset代替位域
int 枚举模式
public class Text {
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles) {
//...
}
}
EnumSet模式
public class Text {
public enum Style {
BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
}
// Any Set could be passed in, but EnumSet is clearly best
public void applyStyles(Set<Style> styles) {
// Body goes here
}
}
- EnumSet 实现了 Set 接口,提供了丰富的功能,类型安全.可以从其他任何
Set
中得到互换性.- 整个 EnumSet 就是用 单个 long 来表示的,
性能上比得上 位运算的性能.
- 总而言之因为枚举类型要用在
集合(Set)
中,所以没有理由用位域
来表示.
33.用enummap代替序数索引
简介
这里的总体原则 和上一个 一致, 就是 尽量不要使用
ordinal()
方法.
以枚举序数
作为数组索引
总不是那么精确.
建议使用 EmumMap
来索引数组,如果是 多维的 ,可以使用 Enum<...,Enum<?>>
书中 示例代码: Herb.java
34.用接口模拟可伸缩的枚举
简介
- 如果让一个
枚举类型
去扩展另一个枚举类型
,利用语言的特性,几乎是不可能的/- 枚举的可扩展性,到最后都证明不是一个好点子.
接口模拟枚举的伸缩性
鉴于如上两点,我们可以利用枚举定义接口
和操作码类型
来扩展枚举,使枚举具有可扩展性. 示例代码: Operation.java
一.定义如下接口
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;
}
},
//...
}
三. 扩展实现类型
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
//...
}
总结
- 从示例中可以学到
泛型继承
的更深层次用法,如
// test parameter is a bounded type token (Item 29)
private static <T extends Enum<T> & Operation> void test(Class<T> opSet,
double x, double y) {
for (Operation op : opSet.getEnumConstants())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
确保了
Class
既是枚举类型
,又是Operation
的子类型
- 虽然无法编写可扩展的枚举类型,却可以通过接口来模拟其伸缩性.
- 这里唯一不足,如上的
保存和获取
与某项操作相关联的符号的逻辑代码,仍无法复用,最好建立辅助类helper
来操作.
35.注解优先于命名模式
简介
在1.5
之前,Java
使用的 是命名模式
,如JUnit
,这种模式有以下缺点:
- 拼写错误不能及时发现
- 无法保证
命名
只在正确的场景使用 - 没有
值/类型信息
,编译器无法提前发现问题
使用 注解
可以很好的解决 如上问题,通过元注解进行约定
-
@Retention
: 限定保留时期 -
@Target
: 限定其应用的程序元素 - 还有很多注解,如
@IntDef
,@ViewDebug
… - 注解接收的参数如果是数组,为其赋值一个单独的元素也是合法的,如下
@ExceptionTest({
IndexOutOfBoundsException.class,NullPointerException.class
})
36.坚持使用override属性
简介
@override 注解表明,被注解的
方法声明
覆盖了超类中的方法声明.
如果使用此注解,而不是超类
中的方法声明,就会报编译时错误
所以,最好是在 每个想要覆盖超类的方法
声明上添加 @override
注解
37.用标记接口定义类型
简介
标记接口就是
不包含方法声明
的接口.指明一个实现类 具有 某种属性
如Serializable
表明实现类 可以被写到ObjectOutputStream
中
在 第 35item
中了解过 标记注解
,在这里所说的是标记接口
.
标记接口
相当于 标记注解
有如下特点
- 标记接口定义的
类型
是由被标记的类的实例
实现的,标记注解
则没有定义这样的类型 - 他们 可以被 更加精确的进行锁定,比如
如果注解类型利用
@Target(ElementType.TYPE)
标记,则它可以被应用到任何类或者接口上
标记注解
胜过 标记接口
的地方
- 它可以通过
默认的方式
添加一个或者多个注解类型元素
,给已被使用的注解
类型添加更多的信息.随着时间的推移,简单的标记注解类型
可以演变成更加丰富的注解类型.
- 他们是更大的注解机制的一部分.
何时使用注解
,何时使用接口
- 如果
标记
是应用到任何程序元素
而不是类或者接口
,就必须使用注解.
因为只有类和接口可以用来实现或者扩展接口
- 如果
标记
只应用给类和接口
,就应该优先使用标记接口而非注解
总而言之
- 如果想要定义 一个任何新方法都不会与之关联的类型,标记接口是最好的选择
- 如果想要标记程序元素而非类和接口,考虑到未来可能要给标记添加更多信息,标记注解是更好的选择.