美文网首页
JAVA枚举

JAVA枚举

作者: Roger_黄 | 来源:发表于2017-04-23 18:22 被阅读0次

一、什么是枚举?

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

二、枚举的优势

我们经常会定义存放常量的类,里面有一些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也会跟着变化。(前面有提到过)

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

相关文章

  • 枚举

    枚举 wiki Java的枚举类型用法介绍 深入理解Java枚举类型(enum) 为什么要用enum? 学习计划 ...

  • Java枚举

    Java中的每个枚举都是java.lang.Enum的final子类,枚举类中的每个枚举常量都是该枚举类的一个实例...

  • Kotlin基础---枚举类

    Java的枚举 Kotlin的枚举 枚举是极少数Kotlin声明比Java使用了更多的关键字的例子Kotlin用了...

  • 枚举学习

    java enum枚举类 enum(枚举)类介绍 **java枚举类是一组预定义常量的集合,使用enum关键字声明...

  • Java枚举类

    枚举其实是个特殊的 Java 类,创建枚举需用enum关键字,枚举类都是java.lang.Enum的子类,jav...

  • Java枚举总结

    Java枚举总结 枚举类型比较简单,下面两个文章讲的比较清楚: Java 枚举(enum) 详解7种常见的用法 深...

  • [Kotlin Tutorials 5] 枚举和Sealed C

    枚举和Sealed Class 枚举 首先, Kotlin和Java一样, 也是有枚举类型的: 枚举类型还可以实现...

  • Java 枚举

    问:Java 枚举类比较用 == 还是 equals,有什么区别? 答:java 枚举值比较用 == 和 equa...

  • 店铺注册的dto(页面数据层)

    ShopExecution.java ShopStateEnum.java使用枚举类表示

  • Java枚举

    枚举 普通方式定义(int枚举模式) 枚举方式 Enum抽象类常见方法 Enum是所有 Java 语言枚举类型的公...

网友评论

      本文标题:JAVA枚举

      本文链接:https://www.haomeiwen.com/subject/qcpdzttx.html