一、什么是枚举?
枚举是由一组固定的常量组成的合法值。通过这一定义,我们可以看出枚举的核心在于常量,而且常量是固定的。这里的“固定”,我的理解是:数目固定,内容固定。也就是说常量的数量是固定的,而且常量不会被替换成其他的常量(注:编译/运行时)。
二、枚举的优势
我们经常会定义存放常量的类,里面有一些int常量(通常叫int枚举)。比如:
public static final int STATE_BEGIN = 1;
public static final int STATE_IN_PROCESSING = 2;
public static final int STATE_END = 3;
public static final int AUDIT_APPROVED = 1;
public static final int AUDIT_IN_PROCESS = 2;
public static final int AUDIT_REJECTED = 3;
这样写也没多大问题,但是也会带来一些比较糟糕体验:
1. 不安全。如果有方法接受STATE的作为参数,其实就算把AUDIT的值传进去也没问题。更有甚者,把这些值拿来做数字运算也不会有问题。
2. 不直观。如果要打印这些常量值,你看到的也就是数字,要是数字一多,你还能记起来这些值代表的含义么?
3. 不方便。需要遍历所有的STATE的值或者数量,并没有方便的API可供调用。
类似的,字符串常量(String枚举)也有问题。比如,客户端硬编码而且拼写错了,那最后运行起来会出问题。
枚举的出现正好可以避免这些问题。
1. 安全:把STATE和AUDIT分别定义为枚举,这两个枚举之间是没有办法串场的,更不可能出现运算的情况。
2. 直观:打印出来的枚举可以是一目了然的,也不会出现拼写错误。
3. 方便:有现成的API可供调用,如values()等。
三、枚举的定义
我们都知道JAVA中定义枚举很简单,使用关键字enum就可以了,就像下面这样:
我们都知道,我们定义类使用的是class,那这个enum关键字和class有关系吗?答案是有。其实enum只是JAVA提供给我们的语法糖,在编译之后,就变成了class。怎么验证呢?使用反编译,也就是javap命令。
我们可以看到原来编译之后,enum变成了final class。基于反编译的结果,我们可以得出以下结论:
1. enum就是一种类。
2. enum是不允许被继承的(final class)。
3. enum是继承了类java.lang.Enum。
4. 枚举值是常量(public static final)。
5. 编译器帮助生成了values()方法和valueOf(String s)的方法,还要静态代码块(static {})。注意,这里的方法和代码块都是空的,并不代表里面什么也有。
6. 枚举是单例的。这个是由JAVA语言来确保的。
那我要想看反编译之后的代码实现呢?也可以,不过我们得找工具。这里我用Jad这个插件。我们到Week.class的目录下,使用命令jad -s java Week.class。结果如下:
public final class Week extends Enum
{
public static Week[] values()
{
return (Week[])$VALUES.clone();
}
public static Week valueOf(String name)
{
return (Week)Enum.valueOf(com/fenqile/java/sharing/enums/compare/Week, name);
}
private Week(String s, int i, int order)
{
super(s, i);
this.order = order;
}
public int getOrder()
{
return order;
}
public static final Week MON;
public static final Week TUE;
public static final Week WED;
public static final Week THR;
public static final Week FRI;
public static final Week SAT;
public static final Week SUN;
private final int order;
private static final Week $VALUES[];
static
{
MON = new Week("MON", 0, 1);
TUE = new Week("TUE", 1, 2);
WED = new Week("WED", 2, 3);
THR = new Week("THR", 3, 4);
FRI = new Week("FRI", 4, 5);
SAT = new Week("SAT", 5, 6);
SUN = new Week("SUN", 6, 7);
$VALUES = (new Week[] {
MON, TUE, WED, THR, FRI, SAT, SUN
});
}
}
原来在static代码块初始化了枚举值,而且把这些枚举值全部放到了$VALUES数组中。values方法就是返回$VALUES数组的克隆,valueOf()方法就是接受枚举的名字,返回对应的枚举。
四、枚举的源码实现
通过上面的分析,我们知道enum是继承了Enum这个类,那Enum这个类做了哪些事呢?让我们来看看Enum的源码。
Enum是一个抽象类,实现了Comparable说明Enum之间是可以比较大小的,compareTo方法实现如下:
要比较两个Enum,首先它们必须是定义在同一个枚举下。注意getClass方法和getDeclaringClass方法并不完全等价。比如,当某个枚举定义了自己的方法时,getClass返回的是一个匿名内部类Week$1(并不一定是$1),getDeclaringClass返回的是顶层的枚举类,在本文中是Week。如果满足首要条件,才会比较它们的顺序。这里的ordinal是每个枚举声明的先后顺序,比如在本例中,MON的ordinal要小于TUE的ordinal。如果调换它们的位置,ordinal会反过来。
Enum实现了Serializable接口,说明它是可以序列化的。它是怎么序列化的呢?让我们来看看官方文档怎么说。
文档说,枚举的序列化只会包含它的名字,它的成员变量是不会包含在其中的。序列化时,ObjectOutputStrea把枚举的名字写入字节流(比如本例的MON, TUE......)。在反序列化时,ObjectInputStream读取枚举的名字结合枚举的class,再通过调用Enum.valueOf方法还原枚举。
再来看看valueOf方法。
大家看到第一行就可以猜到,enumType.enumConstantDirectory()应该是一个Map,Map的key是enum的名字(比如"MON", "TUE"),value就是枚举的对象。enumType.enumConstantDirectory()实际上会通过反射的方式调用values方法(上面有提过),返回数组,再遍历数组,把每个枚举放入Map中。
接着我们看看equals方法。
这里大家看清楚。枚举的equals方法是用的==,也就是说,枚举只和自身equals。
还有clone方法。
为什么要抛出异常呢?还记得上面说过的枚举是单例的吗?这里就是为了单例。
五、枚举的常见用法
1. 定义一组常量。这个好理解,比如上面的例子就是。还可以定义一组行星,一组状态等等。
2. 特定于常量的方法实现。每个枚举实现都有抽象的行为,但是具体不一样。比如下面的例子就是四则运算的实现。
在枚举中定义了一个抽象方法,每个枚举实现都必须实现这个方法。当然,也可以不是抽象的,即有缺省实现,与缺省实现相同的枚举不用写方法。
3. 策略枚举
举个例子,用一个枚举来表示薪资包的工作天数。周一到周五超过8个小时的时间都算加班工资,周六和周日全天都算加班工资。代码如下:
public 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);
}
private enum PayType {
WEEKDAY {
@Override
double overtimePay(double hrs, double payRate) {
return hrs <= HOURS_PER_SHIFT ? 0 : (hrs - HOURS_PER_SHIFT) * payRate /2;
}
},
WEEKEND {
@Override
double overtimePay(double hrs, double payRate) {
return hrs * 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);
}
}
}
我们这里定义了一个私有的枚举类PayType,它负责工资计算,而外面的PayrollDay只负责维护工作天,将计算工资的工作委托给PayType,这样PayrollDay就不用写一些特定于常量的实现了,比如周六周天必须按照加班工资来。这样做更加安全灵活。
六、枚举的集合
1. 枚举的set
如果我想要一个枚举的set,该怎么做呢?
通常我们看到这个需求的第一眼就是使用HashSet,但是有没有更好的办法呢?有的!它就是EnumSet。EnumSet底层根据枚举个数,采用不同的实现。如果个数不超过64,使用RegularEnumSet,否则使用JumboEnumSet。
如果不超过64个,EnumSet底层会用单个long的二进制来表示整个Set中有哪些枚举,也就是源码中的elements。
怎么证明呢?还是看源码。不如我们来看看add方法是怎么实现的。
把1向左移对应的枚举的顺序值之后,elements会把自己和这个值进行按位或操作,再赋给自己。比如,现在elements=5(二进制表示为0000000000000101),也就是代表Enum中的第一个和第三个枚举在其中。现在我要把第二个加入其中(ordinal=1),那就是5与2进行或操作,等于7(二进制表示为0000000000000111),最后三个1代表现在枚举的第一、第二和第三个值都在Set中了。那用HashSet有什么不一样呢?其实HashSet底层用的就是HashMap,HashMap在查找的时候,会先定位桶的位置,然后在桶中通过equals方法在链表中寻找元素。这样一比较,直接通过位操作就可以将集合表示出来,性能是不是会比HashSet好多了?
再来看看contains方法,是不是也印证了elements的功效?
当枚举个数多于64,EnumSet会使用JumboEnumSet来表示。JumboEnumSet与RegularEnumSet的主要区别在与,JumboEnumSet用的是long[]数组来表示。那long[]是怎么表示的呢?很简单,序号为0~63的枚举值的表示,放在数组的第一个元素,64~127的表示放在数组的第二个元素,一次类推。即每64个枚举,换一个long表示。
是不是EnumSet的性能会一直优于HashSet呢?也并不一定。随着枚举个数的增长,特别是大于64个之后,性能会出现下滑。大家可以自己做个实验,比较EnumSet和HashSet的性能。这里就不再详述了。
常见的API有:EnumSet.of(), EnumSet.allOf()。
2. 枚举的map
如果我想要一个枚举的map,key为枚举,该怎么做呢?
类似地,也有一个专门的API:EnumMap。EnumMap底层使用了两个数组,一个Map主键的数组keyUniverse,一个键值的数组vals,如下所示。它的工作原理很简单,以put为例。先找到key在keyUniverse数组中的位置,再取vals数组的同一个位置的值,如果为null,就增加成功。那这里有个问题,如果put的时候,传的value是null,那取得时候还能判断这个key是否已经存过了?不用担心,put的时候,会有一个maskNull的操作,value=null,在存入vals数组的时候,会存成EnumMap内部定义的一个叫NULL的常量(虽然叫NULL,但是不是null),详见put方法。
七、枚举的注意事项
有一点需要注意:不要根据Enum的ordinal到处与它关联的值。设计之初,ordinal只是为了给EnumSet和EnumMap使用的。如果枚举的先后顺序发生变化,ordinal也会跟着变化。(前面有提到过)
那如果真的需要一个这样的值怎么办呢?那就加一个成员变量进去,单独维护。