枚举类是Java5引进的特性,其目的是替换int枚举模式或者String枚举模式,使得语义更加清晰,另外也解决了行为和枚举绑定的问题.
在枚举类之前该模式被广泛使用,如果是int类型常量就被成为int枚举模式,同理是字符串类型常量则是String枚举模式.
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,这显然是不可接受的
public class Fruit {
public static final int APPLE = 1;
}
// 该条件将成功
Assert.assertTrue(Plante.EARTH == Fruit.APPLE);
2. 枚举类与其行为无法很好的绑定 枚举类与行为绑定的操作一般使用switch-case来进行操作,这模式有缺点,比如增加了一个新的枚举常量,但是switch-case中没有增加,这是常有的事情,因为switch-case少一个分支并不会导致编译错误,这种问题很难暴露出来.
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;
}
}
枚举类实质上是一种语法糖,比如下面这个空枚举.
public enum PlanetEnum {
}
反编译(asm-bytecode-intellij)后为
public final class PlanetEnum
extends Enum<PlanetEnum> {
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 {
// 当枚举字段时在这里放入到数组
$VALUES = new PlanetEnum[0];
}
}
能够看出要点:
Enum
,并且final
类,所以自定义枚举类无法继承与被继承.但是可以实现接口Enum
中的name与ordinal.从反编译的代码来看枚举类是可以实现接口的,那么就可以利用接口定义行为,然后枚举类中覆盖行为.同样假设每一个枚举字段所对应的行为不同,那么直接内部覆盖掉也是很好的策略,这种情况下也叫策略枚举模式.(比如计算器实现加减乘除,都是二元操作符,那么策略枚举就很适合,可以动手试试)
public enum PlanetEnum implements Supplier<String>{
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 constant’s 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 ignored–all enum types have a fixed serialVersionUID of 0L.
大概意思是枚举类的序列化依靠的是name
字段,序列化时转成对应的name输出,反序列化时再依靠valueOf()
方法得到对应的枚举字段,从而保证了单例. 并且枚举类的反序列化过程不可定制,入口封住后那么就能彻底保证单例.
那么为什么有很多公司禁止在二方库中返回值或者POJO使用枚举类呢?先看下valueOf
方法也就是反序列化的实现
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
注意当中找不到对应的枚举类时直接抛IllegalArgumentException异常,直接导致返序列化失败,那么本次调用就会失败.这种行为主要出现在对于同一个二方库新版本新增枚举类字段,服务端升级了版本,而客户端端没升级版本,那么整个流程自然会在服务端处理完成后造成失败,既浪费了服务端的计算性能,又没得到想要的结果,自然属于严重事故了.
关于使用建议,参考阿里巴巴Java开发手册中的三条建议,以及笔者的一条建议