[b]本章内容:[/b]
1. 用enum代替int常量
2. 用实例域代替序数
3. 用EnumSet代替位域
4. 用EnumMap代替充数索引
5. 用接口模拟可伸缩的枚举
6. 注解优先于命名模式
7. 坚持使用Override注解
8. 用标记接口定义类型
[b]1. 用enum代替int常量[/b]
枚举类型是指由一组固定的常量组成合法值的类型,该特征是在Java 1.5 中开始被支持的,之前的Java代码都是通过“公有静态常量域字段”的方法来简单模拟枚举的,如:
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
... ...
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这样的写法是比较脆弱的。首先是没有提供相应的类型安全性,如两个逻辑上不相关的常量值之间可以进行比较或运算(APPLE_FUJI - ORANGE_TEMPLE)。再有就是常量int是编译时常量,被直接编译到使用他们的客户端中。如果与该常量关联的int发生了变化,客户端就必须重新编译。如果没有重新编译,程序还是可以执行,但是他们的行为将不确定。
下面我们来看一下Java 1.5 中提供的枚举的声明方式:
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
Java的枚举本质上是int值。Java枚举类型背后的基本想法非常简单,就是通过公有的静态final域为每个枚举常量导出实例的类,因为没有构造器,枚举类型是真正的final。因为客户端既不能创建枚举类型的实例,也不能对它进行扩展。换句话说,枚举类型是实例受控的,它们是的泛型化,本质上是单元素的枚举。
和“公有静态常量域字段”不同的是,如果函数的参数是枚举类型,如Apple,那么他的实际值只能来自于该枚举所声明的枚举值,即FUJI, PIPPIN, GRANNY_SMITH,且一定是三个中的一个。如果试图将Apple和Orange中的枚举值进行比较,将会导致编译错误。
你可以增加或者重新排列枚举类型中的常量,而无需重新编译它的客户端代码,因为导出学时的域在枚举类型和它的客户端之间提供了一个隔离层:常量值并没有被编译到客户端代码中,而是在int枚举模式中。
和C/C++中提供的枚举不同的是,Java中允许在枚举中添加任意的方法和域,并实现任意的接口。它们提供了所有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;
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;
}
}
在上面的枚举示例代码中,已经将数据和枚举常量关联起来了,因此需要声明实例域字段,同时编写一个带有数据并将数据保存在域中的构造器。枚举天生就是不可变的,因此所有的域字段都应该为final的,并最好将它们做成私有的并提供公有的访问方法。下面看一下该枚举的应用示例:
public class WeightTable {
public static void main(String[] args) {
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));
}
}
程序输出:
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
枚举的静态方法values()将按照声明顺序返回他的值数组。枚举的toString方法返回每个枚举值的声明名称。就像其它类一样,除非迫不得已要将枚举方法导出至它的客户端,否则都应该将它声明为私有的,如有必要,则声明为包级私有有。
在实际的编程中,我们常常需要针对不同的枚举常量提供不同的数据行为,见如下代码:
public enum Operation {
PLUS,MINUS,TIMES,pIDE;
double apply(double x,double y) {
switch (this) {
case PLUS: return x + y;
case MINUS: return x - y;
case TIMES: return x * y;
case pIDE: return x / y;
}
throw new AssertionError("Unknown op: " + this);
}
}
上面的代码已经表达出这种根据不同的枚举值,执行不同的操作。如果没有throw语句它就不能进行编译。但是上面的代码在设计方面确实存在一定的缺陷,或者说漏洞,如果我们新增枚举值的时候,所有和apply类似的域函数,都需要进行相应的修改,如有遗漏将会导致异常的抛出。
幸运的是,Java的枚举提供了一种更好的方法可以将不同的行为与每个枚举常量关联起来:在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主体中,用具体的方法覆盖每个常量的抽象apply方法,这种方法称作特定于常量的方法实现 。如下:
public enum Operation {
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;} },
pIDE { double apply(double x,double y) { return x / y;} };
abstract double apply(double x, double y);
}
这样在添加新枚举常量时就不会轻易忘记提供相应的apply方法了,因为该方法就紧跟在每个常量之后,即使你真的忘了,编译器也会提醒你,因为枚举中的抽象方法必须被它所有常量中的具体方法所覆盖。我们在进一步看一下如何将枚举常量和特定的数据进行关联,如下覆盖toString方法示例:
public enum Operation {
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;} },
pIDE("/") { double apply(double x,double y) { return x / y;} };
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
}
下面给出以上代码的应用示例:
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}
输出如下:
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
枚举类型有一个自动产生的valueOf(String)方法,他将常量的名字转变为枚举常量本身,如果在枚举中覆盖了toString方法(如上例),就需要考虑编写一个fromString方法,将定制的字符串表示法变回相应的枚举,见如下代码:
public enum Operation {
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;} },
pIDE("/") { double apply(double x,double y) { return x / y;} };
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
// 新增代码
private static final Map stringToEnum = new HashMap();
static {
for (Operation op : values())
stringToEnum.put(op.toString(),op);
}
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
}
需要注意的是,我们无法在枚举常量构造的时候将自身放入到Map中。与此同时,枚举构造器不可以访问枚举的静态域,除了编译时的常量域之外。
特定于常量的方法有一个美中不足的地方,它们使得在枚举常量中共享代码变得更加困难了。为枚举常量添加一个公有的行为方法时,如果添加了一个新的元素到枚举,但是忘记给switch语句添加相应的case,这是非常危险的。幸运的时,有一种很好的方法可以实现这一点,将这种行为方法移到一个私有的嵌套枚举中,称之为策略枚举。如下:
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);
}
// 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);
}
}
}
如上,如果多个枚举常量同时共享相同的行为,则考虑策略枚举。
[b]2. 用实例域代替序数[/b]
Java中的枚举提供了ordinal()方法,它返回每个枚举常量在类型中的数字位置。你可以试着从序数中得到关联的int值:
public enum Color {
WHITE,RED,GREEN,BLUE,ORANGE,BLACK;
public int indexOfColor() {
return ordinal() + 1;
}
}
上面的枚举中提供了一个获取颜色索引的方法(indexOfColor),该方法将返回颜色值在枚举类型中的声明位置,如果我们的外部程序依赖了该顺序值,那么这将会是非常危险和脆弱的,因为一旦这些枚举值的位置出现变化,或者在已有枚举值的中间加入新的枚举值时,都将导致该索引值的变化。该条目推荐使用实例域的方式来代替枚举提供的序数值,见如下修改后的代码:
public enum Color {
WHITE(1),RED(2),GREEN(3),ORANGE(4),BLACK(5);
private final int indexOfColor;
Color(int index) {
this.indexOfColor = index;
}
public int indexOfColor() {
return indexOfColor;
}
}
Enum规范中谈到ordinal时这么写道:“大多数程序员都不需要这个方法。它是设计成用于像EnumSet和EnumMap这种基于枚举的通用数据结构的。”除非你在编写的是这种数据结构,否则最好避免使用ordinal()方法。
[b]3. 用EnumSet代替位域[/b]
如果一个枚举类型的元素主要用在集合中,一般就使用int枚举模式,将2的不同倍数赋予每个常量:
下面的代码给出了位域的实现方式:
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) { ... }
}
这种表示法让你用OR位运算将几个常量合并到一个集合中,称为位域。使用方式如下:
text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);、
位域表示法也允许利用位操作,有效地执行像union和intersection这样的集合操作。但位域有着int枚举常量的所有缺点,甚至更多。当位域以数字形式打印里,翻译位域比翻译简单的int枚举常量要困难得多。
Java.util包提供了EnumSet类来有效地表示从单个枚举类型中提取的多个值的多个集合,该类实现了Set接口,同时也提供了丰富的功能,类型安全性,以及可以从任何其他Set实现中得到的互用性。但是在内部具体实现上,每个EnumSet内容都表示为位矢量。如果底层的枚举类型有64个或者更少的元素,整个EnumSet就用单个long来表示,因此他的性能也是可以比肩位域的。与此同时,他提供了大量的操作方法,其实现也是基于位操作的,但是相比于手工位操作,由于EnumSet替我们承担了这部分的开发,从而也避免了一些容易出现的低级错误,代码的美观程度也会有所提升,见如下修改的代码:
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
public void applyStyles(Set