int 枚举模式
枚举类
enum 语法糖
如何与行为绑定
线程安全问题
序列化问题
使用建议
枚举类是 Java5 引进的特性, 其目的是替换 int 枚举模式或者 String 枚举模式, 使得语义更加清晰, 另外也解决了行为和枚举绑定的问题.
int 枚举模式 #
在枚举类之前该模式被广泛使用, 如果是 int 类型常量就被成为 int 枚举模式, 同理是字符串类型常量则是 String 枚举模式.
1 2 3 4 5 6 7 8
| public class Plante { public static final int MERCURY = 1; public static final int VENUS = 2; public static final int EARTH = 3; } |
该模式的缺点有很多:
1. Java 作为强类型语言, 该模式让其失去了强类型优势.
举个例子, 假设我又有下面一个枚举类, 那么执行
Plante.EARTH == Fruit.APPLE
结果将为 true, 这显然是不可接受的
1 2 3 4 5
| public class Fruit { public static final int APPLE = 1; } // 该条件将成功 < br ow="0" oh="0">Assert.assertTrue(Plante.EARTH == Fruit.APPLE); |
2. 枚举类与其行为无法很好的绑定
枚举类与行为绑定的操作一般使用 switch-case 来进行操作, 这模式有缺点, 比如增加了一个新的枚举常量, 但是 switch-case 中没有增加, 这是常有的事情, 因为 switch-case 少一个分支并不会导致编译错误, 这种问题很难暴露出来.
1 2 3 4 5 6 7 8 9 10 11 12 13
| public static void apply(int n) { switch (n) { case Plante.MERCURY: // do something break; case Plante.VENUS: // do something break; case Plante.EARTH: // do something break; } } |
枚举类 #
enum 语法糖 #
枚举类实质上是一种语法糖, 比如下面这个空枚举.
1 2
| public enum PlanetEnum { } |
反编译 (asm-bytecode-intellij) 后为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public final class PlanetEnum extends Enum private static final /* synthetic */ PlanetEnum[] $VALUES; public static PlanetEnum[] values() { return (PlanetEnum[])$VALUES.clone(); } public static PlanetEnum valueOf(String name) { return Enum.valueOf(PlanetEnum.class, (String)name); } private PlanetEnum(String string,int n) { super((String)string, (int)n); } static { // 当枚举字段时在这里放入到数组 < br ow="0" oh="0"> $VALUES = new PlanetEnum[0]; } } |
能够看出要点:
枚举类默认继承 Enum, 并且 final 类, 所以自定义枚举类无法继承与被继承. 但是可以实现接口
枚举字段是该枚举类的一个静态常量对象, 且用数组存储.
values 实际上是调用 clone 方法, 其会创建新数组, 数组中放入所有枚举字段.
构造函数前两个默认为枚举字段名称, 以及所处的顺序. 也就是 Enum 中的 name 与 ordinal.
如何与行为绑定 #
从反编译的代码来看枚举类是可以实现接口的, 那么就可以利用接口定义行为, 然后枚举类中覆盖行为. 同样假设每一个枚举字段所对应的行为不同, 那么直接内部覆盖掉也是很好的策略, 这种情况下也叫策略枚举模式.(比如计算器实现加减乘除, 都是二元操作符, 那么策略枚举就很适合, 可以动手试试)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public enum PlanetEnum implements Supplier MERCURY(1) { @Override public String get() { return "地球"; } }; private int code; PlanetEnum(int code) { this.code = code; } public int getCode() { return code; } @Override public String get() { return "PLANET"; } } |
线程安全问题 #
反编译后的代码所有枚举字段都是 static final,Jvm 的加载初始化流程保证其只被实例化一次, 且实例化之后不可更改.
枚举类的实例化可以看做为饿汉式的单例, 实际上是一个简单而又有效的模式, 包括 kotlin 的 object 单例关键字也是使用了类似的方式.
序列化问题 #
在 JDK 序列化方式中, ObjectInputStream 类中有如下注释:
- Enum constants are deserialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not transmitted. To deserialize an enum constant,ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the static method
- Enum.valueOf(Class, String)
- with the enum constants base type and the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are deserialized cannot be customized: any class-specific readObject, readObjectNoData, and readResolve methods defined by enum types are ignored during deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignoredall enum types have a fixed serialVersionUID of 0L.
大概意思是枚举类的序列化依靠的是 name 字段, 序列化时转成对应的 name 输出, 反序列化时再依靠 valueOf()方法得到对应的枚举字段, 从而保证了单例. 并且枚举类的反序列化过程不可定制, 入口封住后那么就能彻底保证单例.
那么为什么有很多公司禁止在二方库中返回值或者 POJO 使用枚举类呢? 先看下 valueOf 方法也就是反序列化的实现
1 2 3 4 5 6 7 8 9 10
| public static String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); |
注意当中找不到对应的枚举类时直接抛 IllegalArgumentException 异常, 直接导致返序列化失败, 那么本次调用就会失败. 这种行为主要出现在对于同一个二方库新版本新增枚举类字段, 服务端升级了版本, 而客户端端没升级版本, 那么整个流程自然会在服务端处理完成后造成失败, 既浪费了服务端的计算性能, 又没得到想要的结果, 自然属于严重事故了.
使用建议 #
关于使用建议, 参考阿里巴巴 Java 开发手册中的三条建议, 以及笔者的一条建议
所有的枚举类型字段必须要有注释, 说明每个数据项的用途
二方库里可以定义枚举类型, 参数可以使用枚举类型, 但是接口返回值不允许使用枚 举类型或者包含枚举类型的 POJO 对象(这里返回值不可使用因为有反序列化的问题, 那么为什么参数又可以使用呢? 笔者不太清楚, 希望大牛告知.)
枚举类名建议带上 Enum 后缀, 枚举成员名称需要全大写, 单词间用下划线隔开
枚举类与 switch-case 在外部搭配时要注意, 当枚举类增加字段时就带来 switch-case 的更新问题, 这种 bug 编译期间无法得知, 最好的办法时把行为与枚举类绑定, 或者把 switch-case 的逻辑统一写在该枚举类的内部.
来源: https://juejin.im/entry/5aa36a0ff265da23750680db