JAVA枚举

一、什么是枚举?

枚举是由一组固定的常量组成的合法值。通过这一定义,我们可以看出枚举的核心在于常量,而且常量是固定的。这里的“固定”,我的理解是:数目固定,内容固定。也就是说常量的数量是固定的,而且常量不会被替换成其他的常量(注:编译/运行时)。

二、枚举的优势

我们经常会定义存放常量的类,里面有一些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的声明

Enum是一个抽象类,实现了Comparable说明Enum之间是可以比较大小的,compareTo方法实现如下:

compareTo方法

要比较两个Enum,首先它们必须是定义在同一个枚举下。注意getClass方法和getDeclaringClass方法并不完全等价。比如,当某个枚举定义了自己的方法时,getClass返回的是一个匿名内部类Week$1(并不一定是$1),getDeclaringClass返回的是顶层的枚举类,在本文中是Week。如果满足首要条件,才会比较它们的顺序。这里的ordinal是每个枚举声明的先后顺序,比如在本例中,MON的ordinal要小于TUE的ordinal。如果调换它们的位置,ordinal会反过来。

Enum实现了Serializable接口,说明它是可以序列化的。它是怎么序列化的呢?让我们来看看官方文档怎么说。

Enum的序列化

文档说,枚举的序列化只会包含它的名字,它的成员变量是不会包含在其中的。序列化时,ObjectOutputStrea把枚举的名字写入字节流(比如本例的MON, TUE......)。在反序列化时,ObjectInputStream读取枚举的名字结合枚举的class,再通过调用Enum.valueOf方法还原枚举。

再来看看valueOf方法。

valueOf方法

大家看到第一行就可以猜到,enumType.enumConstantDirectory()应该是一个Map,Map的key是enum的名字(比如"MON", "TUE"),value就是枚举的对象。enumType.enumConstantDirectory()实际上会通过反射的方式调用values方法(上面有提过),返回数组,再遍历数组,把每个枚举放入Map中。

enumConstantDirectory方法
反射拿到枚举数组

接着我们看看equals方法。

equals方法

这里大家看清楚。枚举的equals方法是用的==,也就是说,枚举只和自身equals。

还有clone方法。

clone方法

为什么要抛出异常呢?还记得上面说过的枚举是单例的吗?这里就是为了单例。

五、枚举的常见用法

1. 定义一组常量。这个好理解,比如上面的例子就是。还可以定义一组行星,一组状态等等。

2. 特定于常量的方法实现。每个枚举实现都有抽象的行为,但是具体不一样。比如下面的例子就是四则运算的实现。

constant-specific method implementation

在枚举中定义了一个抽象方法,每个枚举实现都必须实现这个方法。当然,也可以不是抽象的,即有缺省实现,与缺省实现相同的枚举不用写方法。

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。

elements

怎么证明呢?还是看源码。不如我们来看看add方法是怎么实现的。

RegularEnumSet.add(E e)

把1向左移对应的枚举的顺序值之后,elements会把自己和这个值进行按位或操作,再赋给自己。比如,现在elements=5(二进制表示为0000000000000101),也就是代表Enum中的第一个和第三个枚举在其中。现在我要把第二个加入其中(ordinal=1),那就是5与2进行或操作,等于7(二进制表示为0000000000000111),最后三个1代表现在枚举的第一、第二和第三个值都在Set中了。那用HashSet有什么不一样呢?其实HashSet底层用的就是HashMap,HashMap在查找的时候,会先定位桶的位置,然后在桶中通过equals方法在链表中寻找元素。这样一比较,直接通过位操作就可以将集合表示出来,性能是不是会比HashSet好多了?

再来看看contains方法,是不是也印证了elements的功效?

RegularEnumSet.contains(Object e)

当枚举个数多于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方法。

keyUniverse & vals
EnumMap.put(K key, V value)

七、枚举的注意事项

有一点需要注意:不要根据Enum的ordinal到处与它关联的值。设计之初,ordinal只是为了给EnumSet和EnumMap使用的。如果枚举的先后顺序发生变化,ordinal也会跟着变化。(前面有提到过)

那如果真的需要一个这样的值怎么办呢?那就加一个成员变量进去,单独维护。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 概念 enum 的全称为 enumeration, 是 JDK 1.5 中引入的新特性。 在Java中,被 en...
    静默虚空阅读 8,449评论 1 18
  • 一 Java 枚举7常见种用法DK1.5引入了新的类型——枚举。在 Java 中它虽然算个“小”功能,却给我的开发...
    欢乐时光欢乐你我阅读 861评论 0 6
  • 枚举可以实现接口 enum 可以像一般类一样实现接口。 同样是实现上一节中的错误码枚举类,通过实现接口,可以约束它...
    java部落阅读 168评论 0 1
  • 枚举的特性 枚举的特性,归结起来就是一句话: 除了不能继承,基本上可以将 enum 看做一个常规的类。 但是这句话...
    java部落阅读 335评论 0 2
  • UItextfield 是有占位文字的,但是不能换行,UITextView可以输入多文本,但是没有占位文字的功能;...
    伊蕊飘零阅读 1,169评论 0 0