30、用enum代替int常量
枚举类型是指由一组固定的常量组成合法值的类型。在java没有引入枚举类型前,表示枚举类型的常用方法是声明一组不同的int常量,每个类型成员一个常量,这种方法称作int枚举模式。采用int枚举模式的程序是十分脆弱的,因为int值是编译时常量,若与枚举常量关联的int发生变化,客户端就必须重新编译。
java枚举类型背后的思想:通过公有的静态final域为每个枚举常量导出实例的类。因为没有可以访问的构造器,枚举类型是真正的final。客户端既不能创建枚举类型的实例,也不能对它进行扩展。枚举类型是实例受控的,它们是单例的泛型化,本质上是单元素的枚举。枚举提供了编译时的类型安全。
包含同名常量的多个枚举类型可以在一个系统中和平共处,因为每个类型都有自己的命名空间。可以增加或重新排列枚举类型中的常量,而无需重新编译客户端代码,因为常量值并没有被编译到客户端代码中。可以调用toString方法,将枚举转换成可打印的字符串。
枚举类型允许添加任意的方法和域,并实现任意的接口。枚举类型默认继承Enum类(其实现了Comparable、Serializable接口)。为了将数据与枚举常量关联起来,得声明实例域,并编写一个将数据保存到域中的构造器。枚举天生就是不可变的,所有的域都必须是final的。
例如:
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; //质量kg
private final double radius; //半径
private final double surfaceGravity; //表面重力,final常量构造器中必须初始化
private static final double G = 6.673E-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 static void main(String[] args) {
double earthWeight = 175;
double mass = earthWeight/Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
//java的printf方法中换行用%n, C语言中用\n
System.out.printf("Weight on %s is %f%n", p, p.surfaceWeight(mass));
}
}
}
上面的方法对大多数枚举类型来说足够了,但有时你需要将本质上不同的行为与每个常量关联起来。这时通常需要在枚举类型中声明一个抽象的apply方法,并在特定于常量的类主题中实现这个方法。这个方法被称作特定于常量的方法实现。例如:
public enum Operation {
PULS("+") {
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;
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 = 2.0;
double y = 4.0;
for(Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x,y));
}
}
枚举类型中的抽象方法,在它的常量中必须被实现。除了编译时常量之外,枚举构造器不可以访问枚举的静态域,因为构造器运行时,静态域还没被初始化。
特定于常量的方法,使得在枚举常量中共享代码变的更加困难。例如:根据给定的工人的基本工资(按小时算)和工作时间,用枚举计算工人当天的工作报酬。其中加班工资为平时的1.5倍。
public enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURADAY, FRIDAY, SATURDAY, SUNDAY;
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate) {
switch(this) {
case SATURDAY: case SUNDAY :
return hoursWorked*payRate*1.5;
default :
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
上面这段代码虽然十分简洁,但是维护成本很高。每将一个元素添加到该枚举中,就必须修改switch语句。可以使用策略枚举来进行优化,例如:
public enum PayrollDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURADAY(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 {
double pay(double hoursWorked, double payRate) {
return hoursWorked - HOURS_PER_SHIFT > 0
?(hoursWorked*payRate*1.5 - 0.5*HOURS_PER_SHIFT*payRate)
: hoursWorked*payRate;
}
},
WEEKEND {
double pay(double hoursWorked, double payRate) {
return hoursWorked * payRate * 1.5;
}
};
private static final int HOURS_PER_SHIFT = 8;
abstract double pay(double hoursWorked, double payRate);
}
public static void main(String[] args) {
System.out.println(PayrollDay.MONDAY.pay(10,10));
System.out.println(PayrollDay.SUNDAY.pay(10,10));
}
}
总之,与int常量相比,枚举类型优势明显。许多枚举都不需要显式的构造器或成员。当需要将不同的行为与每个常量关联起来时,可使用特定于常量的方法。若多个枚举常量同时共享相同的行为,考虑使用策略枚举。
31、用实例域代替序数
所有的枚举都有一个ordinal方法,它返回枚举常量在类中的位置。若常量进行重排序,它们ordinal的返回值将发生变化。所以,永远不要根据枚举的序数导出与它关联的值,而是要将它保存在一个实例域中。
public enum Planet {
MERCURY(1),
VENUS(2),
EARTH(3),
MARS(4),
JUPITER(5),
SATURN(6),
URANUS(7),
NEPTUNE(8);
private final int numOrd;
Planet(int numOrd) {this.numOrd = numOrd; }
public int numOrd(){ return numOrd; }
}
Enum规范中谈到ordinal时写道:它是用于像EnumSet和EnumMap这种基于枚举的数据结构的方法,平时最好不要使用它。
32、用EnumSet代替位域
若枚举类型要用在集合中,可以使用EnumSet类。EnumSet类是专为枚举类设计的集合类,EnumSet中的所有元素都必须是单个枚举类型中的枚举值。若元素个数小于64,整个EnumSet就用一个long来表示,所以它的性能比的上位域(通过位操作实现)的性能。
import java.util.*;
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