枚举类型(enum type)
是指一组固定的常量组成合法值的类型,例如一年中的季节、太阳系中的行星或者一副牌中的花色。
int枚举模式的缺点
在我们平常的开发中,为表示同种类型的不同种类,经常的做法是声明一组具名的int常量来表示,每个类型成员一个常量,如:
public static final int DAY_MONDAY = 1;
public static final int DAY_TUESDAY = 2;
public static final int DAY_WEDNESDAY = 3;
public static final int DAY_THURSDAY = 4;
public static final int DAY_FRIDAY = 5;
public static final int DAY_SATURDAY = 6;
public static final int DAY_SUNDAY = 7;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
这种方法称做 int枚举模式,这种方式在安全性和使用方便性方面没有任何帮助。
a、将day传到想要orange的方法中,编译器不会警告,执行也不会出现错误;
b、用==操作符将day与orange比较,编译器不会警告,执行也不会出现错误;
c、int枚举是编译时常量,被编译到客户端中,如果枚举常量关联的int发生变化,客户端必须重新编译,如果没有重新编译,程序仍可以运行,但行为就不确定了,如DAY_MONDAY关联的常量不再是1,而是0。
d、将int枚举常量翻译成可打印的字符串很麻烦
e、如果想要遍历一个组中的所有int 枚举常量,甚至获得int枚举组的大小,这种实现没有啥方便可靠的方法
因此,推荐使用枚举类型来代替这种int枚举常量:
public enum DAY {
DAY_MONDAY,
DAY_TUESDAY,
DAY_WEDNESDAY,
DAY_THURSDAY,
DAY_FRIDAY,
DAY_SATURDAY,
DAY_SUNDAY
}
public enum ORANGE {
ORANGE_NAVEL,
ORANGE_TEMPLE,
ORANGE_BLOOD
}
这种枚举类型,提供了编译时的类型安全检查,如果声明了一个参数的类型为DAY,就可以保证,被传到该参数上的任何非null的对象引用一定属于其他有效值中的一个,试图传递类型错误的值时,会导致编译时错误,就像试图将某种枚举类型的表达式赋给另一种枚举类型的变量,或者试图利用==操作符比较不同枚举类型的值一样。同时包含同名常量的多个枚举类型可以共存,因为每个类型有自己的命名空间,增加或重新排列枚举类型的常量,无需重新编译客户端的代码。如果想获取类型对应的字符串,直接通过toString方法即可。
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);
}
public static void main(String[] args) {
Operation divide = Operation.DIVIDE;
double apply = divide.apply(1, 2);
System.out.println(apply);
}
}
大家一开始都会这样写的。实际开发中,有很多开发者也这样写。但是有个不足:如果需要新增加运算,譬如模运算,不仅仅需要添加枚举类型常量,还需要修改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;
abstract double apply(double x, double y);
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public static void main(String[] args) {
Operation minus = Operation.MINUS;
double apply = minus.apply(1, 2);
System.out.println(minus.toString());
System.out.println(apply);
}
}
一般,enum
中重写了toString
方法之后,enum
中自生成的valueOf(String)
方法不能根据枚举常量的字符串(toString
生成)来获取枚举常量。
Operation operation = Operation.valueOf("DIVIDE");
我们通常需要在enum
中新增个静态常量来获取。如:
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 = new HashMap();
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);
}
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
}
}
可以通过调用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);
}
}
}
虽然看起来代码不够简洁,但是修改起来确实比较安全,不怕有遗漏。
枚举的实现原理
枚举是通过共有的静态final域为每个枚举常量导出实例的类,因为没有可以访问的构造器,枚举类型是真正的final,因为客户端既不能创建枚举类型的实例,也不能对他进行扩展,因为很可能没有实例,而只有声明过的枚举常量。换句话说,枚举类型是实例受控的,他们是单例的泛型化,本质上是单元素的枚举。如我们上面的ORANGE枚举,
public enum ORANGE {
ORANGE_NAVEL,
ORANGE_TEMPLE,
ORANGE_BLOOD;
}
在编译完成之后,生成的字节码反编译之后
public final class ORANGE extends Enum
{
public static ORANGE[] values()
{
return (ORANGE[])$VALUES.clone();
}
public static ORANGE valueOf(String name)
{
return (ORANGE)Enum.valueOf(com/example/demo/ORANGE, name);
}
private ORANGE(String s, int i)
{
super(s, i);
}
public static final ORANGE ORANGE_NAVEL;
public static final ORANGE ORANGE_TEMPLE;
public static final ORANGE ORANGE_BLOOD;
private static final ORANGE $VALUES[];
static
{
ORANGE_NAVEL = new ORANGE("ORANGE_NAVEL", 0);
ORANGE_TEMPLE = new ORANGE("ORANGE_TEMPLE", 1);
ORANGE_BLOOD = new ORANGE("ORANGE_BLOOD", 2);
$VALUES = (new ORANGE[] {
ORANGE_NAVEL, ORANGE_TEMPLE, ORANGE_BLOOD
});
}
}
可以看到
a、枚举类是继承于java.lang.Enum的类。
b、枚举值是类对象, 且是静态常量(被static final修饰)。
c、静态代码块内实例化枚举值,由于静态代码块的语法特性,该代码块只执行一次;
d、默认值0、1、2是在编译时生成的。
小结
总而言之,与int常量相比,枚举类型的优势是不言而喻的。枚举的可读性要好很多,也更加安全,功能也更加强大,许多枚举都不需要显示的构造器或成员,但许多其他枚举则受益于“每个常量与属性的关联”以及“提供行为受这个属性影响的方法”。只有极少数的枚举受益于将多种行为与单个方法关联,在这种相对少见的情况下,特定于常量的方法要优先于启用自由值的枚举,如果多个枚举常量同时共享相同的行为,则考虑策略枚举。