使用enum代替int常量
在枚举类型出现之前,一般都常常使用int常量或者String常量表示列举相关事物。如:
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;
针对int常量以下不足:
- 在类型安全方面,如果你想使用的是ORANGE_NAVEL,但是传递的是APPLE_FUJI,编译器并不能检测出错误;
- 因为int常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int发生了变化,客户端需重新编译,否则它们的行为就不确定;
- 没有便利方法将int常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVEL,debug的时候显示的是0,但你不能确定是APPLE_FUJI还是ORANGE_NAVEL
如果你想使用String常量,虽然提供了可打印的字符串,但是性能会有影响。特殊是对于有些新手开发,有可能会直接将String常量硬编码到代码中,导致以后修改很困难。
所有这一切,enum都给出了具体的解决。唯一的缺点只是需要增加enum加载和实例化的时间。
用enum构建
以上面的APPLE、ORANGE为例,用enum重写:
public enum Apple {
APPLE_FUJI,
APPLE_PIPPIN,
APPLE_GRANNY_SMITH;
}
public enum Orange {
ORANGE_NAVEL,
ORANGE_TEMPLE,
ORANGE_BLOOD;
}
在调用的时候,直接使用enum类型,在编译的时候可以直接指定类型,否则编译不通过;并且debug的时候,显示的是enum中的常量(APPLE_FUJI这样的),可以一眼看出是否用错;最后由于枚举导出的常量域(APPLE_FUJI等)与客户端之间是通过枚举来引用的,再增加或者重排序枚举类型中的常量后,并不需要重新编译客户端代码
enum枚举常量与数据关联
enum枚举常量可以与数据相关,然后在枚举中提供方法返回客户端需要的信息。
如以太阳系为例,每个行星都拥有质量和半径,可以依据这两个属性计算行星表面物体的重量。代码如下:
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; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
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; // F = ma
}
}
public class PlanetDemo {
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));
}
//args[0]=30输出结果
//Weight on MERCURY is 11.337201
//Weight on VENUS is 27.151530
//Weight on EARTH is 30.000000
//Weight on MARS is 11.388120
//Weight on JUPITER is 75.890383
//Weight on SATURN is 31.965423
//Weight on URANUS is 27.145664
//Weight on NEPTUNE is 34.087906
}
}
枚举常量与行为关联
有些时候将enum枚举常量与数据关联还不够,还需要将枚举常量与行为关联。
如采用枚举来写加、减、乘、除的运算。代码如下:
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
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);
}
}
大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改apply方法。万一忘记修改了,那就是运行时错误。将代码修改如下:
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;
}
};
abstract double apply(double x, double y);
}
每次新增加运算种类,都需要重写apply方法,这样就不会有遗漏修改。
你可以写的更详细些:
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 String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
}
public class OperationDemo {
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.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
}
//输入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
}
}
一般,enum中重写了toString方法之后,enum中自生成的valueOf(String)方法不能根据枚举常量的字符串(toString生成)来获取枚举常量。我们通常需要在enum中新增个静态常量来获取。如:
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 String symbol;
public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap();
static {
for (Operation op : Operation.values()) {
OPERS_MAP.put(op.toString(), op);
}
}
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
abstract double apply(double x, double y);
}
可以通过调用Operation.OPERS_MAP.get(op.toString())来获取对应的枚举常量。
在有些特定的情况下,此写法有个缺点,即如果每个枚举常量都有公共的部分处理该怎么办,如果每个枚举常量关联的方法里都有公共的部分,那不仅不美观,还违反了DRY原则。这就是下面的枚举策略模式。
枚举策略模式
enum PayrollDay {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY;
private static final int HOURS_PER_SHIFT = 8;
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay; // Calculate overtime pay
switch(this) {
case SATURDAY: case SUNDAY:
overtimePay = hoursWorked * payRate / 2;
break;
default: // Weekdays
overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
以上代码是计算工人工资。平时工作8小时,超过8小时,以加班工资方式另外计算;如果是双休日,都按照加班方式处理工资。
上面代码的写法和上一小节给出的差不多,通过switch来分拆计算。还是一样的问题,如果此时新增加一种工资的计算方式,枚举常量需要改,pay方法也需要改。按上一小节的介绍继续修改:
enum PayrollDay {
MONDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
return basePay + overtimePay;
}
},
TUESDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
return basePay + overtimePay;
}
},
WEDNESDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
return basePay + overtimePay;
}
},
THURSDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
return basePay + overtimePay;
}
},
FRIDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
return basePay + overtimePay;
}
},
SATURDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = overtimePay = hoursWorked * payRate / 2;
return basePay + overtimePay;
}
},
SUNDAY {
@Override
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
double overtimePay = overtimePay = hoursWorked * payRate / 2;
return basePay + overtimePay;
}
}, ;
private static final int HOURS_PER_SHIFT = 8;
abstract double pay(double hoursWorked, double payRate);
}
看了上面的代码,我觉得大家都不会这样写吧。其实细想一下,最主要的不同就是计算加班时间的工资方式不同,也就是分工作日和双休日的。继续修改:
public enum PayRoll {
MONDY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayRoll(PayType payType) {
this.payType = payType;
}
double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}
private enum PayType {
WEEKDAY {
@Override
double overtimePay(double hoursWorked, double payRate) {
double overtime = hoursWorked - HOURS_PER_SHIFT;
return overtime <= 0 ? 0 : overtime * payRate / 2;
}
},
WEEKEND {
@Override
double overtimePay(double hoursWorked, double payRate) {
return hoursWorked * payRate / 2;
}
};
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);
}
}
}
虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。
补充一点
从上面可以看出,不推荐在enum中使用switch...case...来判断不同的行为。那什么时候可以使用呢?主要是适用于给外部的枚举类型增加特定于常量的行为。如,假设Operation枚举不受开发者自己控制,但是希望它有一个实例方法来返回每个运算的反运算,则可以:
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op: " + op);
}
}