001-基础知识专题-关键字和接口-6、enum
1. 是什么
Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。
Java 枚举类使用 enum 关键字来定义,各个常量使用逗号 ,
来分割。
2. 保证线程安全
1 |
|
2.1. enum 关键字的作用
通过反编译得到内容如下:
1 |
|
通过反编译后代码我们可以看到,public final class T extends Enum
,说明,该类是继承了 Enum
类的,同时 final
关键字告诉我们,这个类也是不能被继承的。当我们使用 enum
来定义一个枚举类型的时候,编译器会自动帮我们创建一个 final
类型的类继承 Enum 类 ,所以枚举类型不能被继承。这个类中有私有构造函数、公共的 values()
和 valueof(String s)
方法、常量类型的枚举内容
以及 给常量赋值的静态代码块
❕
2.2. 为什么线程安全⭐️🔴
我们看到反编译后代码,这个类中有几个属性和方法。
1 |
|
我们可以看到都是 static
类型的,因为 static
类型的属性会在类被加载之后被初始化,当一个 Java
类第一次被主动使用 (性能调优-基础-6、JVM-类装载子系统) 到的时候静态资源被初始化、Java
类的加载和初始化过程都是线程安全的。所以,创建一个 enum
类型是线程安全的。
初始化时,对于
<clinit> ()
方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为<clinit> ()
方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。
然后 new 关键字会创建对象,在创建对象过程中,JVM 会通过TLAB 以及 CAS的方式保证创建对象时的线程安全,综合来说,enum 是线程安全的。
3. 最佳单例实现方式⭐️🔴
Effective Java,
作者,Josh Bloch,
提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?❕
3.1. 枚举写法简单
1 |
|
可以直接通过 EasySingleton.INSTANCE
来访问。
3.2. 枚举自己处理序列化
因为枚举自己处理序列化,而且又无法反射获取到单实例。所以唯一一种不会被破坏的单例实现模式。其他方式都会被反射或者反序列化破坏掉单例模式。❕
我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了 Serializable
接口之后,就不再是单例的了,因为,每次调用 readObject()
方法返回的都是一个新创建出来的对象,有一种解决办法就是使用 readResolve()
方法来避免此事发生。 [[内功心法专题-设计模式-3、单例模式#5 1 2 1 为什么readResolve可以解决⭐️🔴]]
但是,为了保证枚举类型像 Java
规范中所说的那样,每一个枚举类型极其定义的枚举变量在 JVM中
都是唯一的,在枚举类型的序列化和反序列化上,Java
做了特殊的规定。原文如下:
Enum constants are serialized 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 present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with 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 serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
大概意思就是说,在序列化的时候 Java
仅仅是将枚举对象的 name
属性输出到结果中,反序列化的时候则是通过 java.lang.Enum
的 valueOf
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的进行定制,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace
和 readResolve
等方法。我们看一下这个 valueOf
方法: ❕ ^eef92o
1 |
|
从代码中可以看到,代码会尝试从调用 enumType
这个 Class
对象的 enumConstantDirectory()
方法返回的 map
中获取名字为 name
的枚举对象,如果不存在就会抛出异常。再进一步跟到 enumConstantDirectory()
方法中调用了 getEnumConstantsShared()
,就会发现到最后会以反射的方式调用 enumType
这个类型的 values()
静态方法,也就是上面我们看到的编译器为我们创建的那个 values()
方法,然后用返回结果填充 enumType
这个 Class
对象中的 enumConstantDirectory
属性。❕
❕
1 |
|
1 |
|
3.3. 枚举禁止被反射创建
1 |
|
1 |
|
否则报错:Cannot reflectively create enum objects
❕
4. 实战经验
4.1. 开发使用
枚举类中的抽象方法实现,需要枚举类中的每个对象都对其进行实现。
4.2. 服务升级 - 反序列化失败
❕ ^tai2qg
4.2.1. 序列化内容与代码中不一致
在系统或者类库升级时,对其中定义的枚举类型多加注意,为了保持代码上的兼容性,如果我们定义的枚举类型有可能会被序列化保存 (放到文件中、保存到数据库中,进入分布式内存缓存中),那么我们是不能够删除原来枚举类型中定义的任何枚举对象的,否则程序在运行过程中,JVM 拿着之前保存的名称进行反序列化时,就会抱怨找不到与某个名字对应的枚举对象了。
4.2.2. 服务端与客户端不一致
另外,在远程方法调用过程中,如果我们发布的客户端接口返回值中使用了枚举类型,那么服务端在升级过程中就需要特别注意。如果在接口的返回结果的枚举类型中添加了新的枚举值,那就会导致仍然在使用老的客户端的那些应用出现调用失败的情况。
#todo
5. 参考与感谢
https://www.cnblogs.com/z00377750/p/9177097.html
[[深度分析Java的枚举类型—-枚举的线程安全性及序列化问题 - 风动静泉 - 博客园]]