1. 是什么

Java 枚举是一个特殊的类,一般表示一组常量,比如一年的 4 个季节,一个年的 12 个月份,一个星期的 7 天,方向有东南西北等。

Java 枚举类使用 enum 关键字来定义,各个常量使用逗号  ,  来分割。

2. 保证线程安全

1
public enum t { SPRING,SUMMER,AUTUMN,WINTER; }

2.1. enum 关键字的作用

通过反编译得到内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final class T extends Enum
{
private T(String s, int i)
{
super(s, i);
}
public static T[] values()
{
T at[];
int i;
T at1[];
System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
return at1;
}

public static T valueOf(String s)
{
return (T)Enum.valueOf(demo/T, s);
}

public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}

通过反编译后代码我们可以看到,public final class T extends Enum,说明,该类是继承了 Enum 类的,同时 final 关键字告诉我们,这个类也是不能被继承的。当我们使用 enum 来定义一个枚举类型的时候,编译器会自动帮我们创建一个 final 类型的类继承 Enum 类 ,所以枚举类型不能被继承。这个类中有私有构造函数、公共的 values()valueof(String s) 方法、常量类型的枚举内容 以及 给常量赋值的静态代码块%%
1625-🏡⭐️◼️我们用 enum 关键字创建一个 enum 时 JVM 做了什么?会帮我们创建一个 final 修饰的类并继承 Enum 类◼️⭐️-point-202301261625%%

2.2. 为什么线程安全⭐️🔴

我们看到反编译后代码,这个类中有几个属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];

static{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}

我们可以看到都是 static 类型的,因为 static 类型的属性会在类被加载之后被初始化,当一个 Java 类第一次被主动使用 (性能调优-基础-6、JVM-类装载子系统) 到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个 enum 类型是线程安全的。

初始化时,对于 <clinit> () 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 <clinit> () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现。

然后 new 关键字会创建对象,在创建对象过程中,JVM 会通过TLAB 以及 CAS的方式保证创建对象时的线程安全,综合来说,enum 是线程安全的。

3. 最佳单例实现方式⭐️🔴

Effective Java, 作者,Josh Bloch, 提倡使用枚举的方式,既然大神说这种方式好,那我们就要知道它为什么好?❕%%
0846-🏡⭐️◼️为什么枚举是最佳单例实现?写法简单;自己处理反序列化,不会破坏单例;禁止被反射创建◼️⭐️-point-202301230846%%

3.1. 枚举写法简单

1
2
3
public enum EasySingleton{
INSTANCE;
}

可以直接通过 EasySingleton.INSTANCE 来访问。

3.2. 枚举自己处理序列化

因为枚举自己处理序列化,而且又无法反射获取到单实例。所以唯一一种不会被破坏的单例实现模式。其他方式都会被反射或者反序列化破坏掉单例模式。❕%%
0847-🏡⭐️◼️单例被破坏的情况和原因:反射会破坏单例,最后调用 newInstance 生成一个新对象。反序列化同样的原理◼️⭐️-point-202301230847%%

我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了 Serializable 接口之后,就不再是单例的了,因为,每次调用 readObject() 方法返回的都是一个新创建出来的对象,有一种解决办法就是使用 readResolve() %%⭐️解决序列化破坏单例⭐️-point-202301102128%%方法来避免此事发生。 [[内功心法专题-设计模式-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.EnumvalueOf 方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的进行定制,因此禁用了 writeObject、readObject、readObjectNoData、writeReplacereadResolve 等方法。我们看一下这个 valueOf 方法:%%
▶2.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230401-2123%%
❕ ^eef92o

image.png

1
2
3
4
5
6
7
8
9
10
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);
}

从代码中可以看到,代码会尝试从调用 enumType 这个 Class 对象的 enumConstantDirectory() 方法返回的 map 中获取名字为 name 的枚举对象,如果不存在就会抛出异常。再进一步跟到 enumConstantDirectory() 方法中调用了 getEnumConstantsShared(),就会发现到最后会以反射的方式调用 enumType 这个类型的 values() 静态方法,也就是上面我们看到的编译器为我们创建的那个 values() 方法,然后用返回结果填充 enumType 这个 Class 对象中的 enumConstantDirectory 属性。❕%%
1633-🏡⭐️◼️虽然枚举类禁止被反射获取,但枚举类自己处理反序列化时却用到了反射,如此对比也没什么意义,只是想到了这一点。枚举自己处理反序列化时,最终底层通过反射调用了枚举类中的 values 方法获取到了枚举类放到 enumConstantDirectory 中◼️⭐️-point-202301261633%%

%%
🏡⭐️◼️enum 反序列化不破坏单例的原理◼️⭐️-point-202301202254%%

1
2
3
4
5
6
7
8
9
10
11
12
13
Map<String, T> enumConstantDirectory() {  
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
T[] getEnumConstantsShared() {  
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec. catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex) { return null; }
}
return enumConstants;
}

3.3. 枚举禁止被反射创建

1
2
3
4
5
6
7
8
9
10
/**
* 枚举测试类
*/
public enum Etp {
INSTANCE;

public Etp getInstance(){
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
public static void main(String[] args) {
Etp instance = Etp.INSTANCE;
try {
Class simple = Class.forName("com.yuanping.sjms_demo.singleton.Etp");
Constructor declaredConstructor = simple.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Etp o = (Etp)declaredConstructor.newInstance();
System.out.println(instance == o);
} catch (ClassNotFoundException e) {

} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}

}
}

否则报错:Cannot reflectively create enum objects%%
🏡⭐️◼️反射创建枚举对象会报错,newInstance 方法中做了判断处理◼️⭐️-point-202301202254%%

4. 实战经验

4.1. 开发使用

枚举类中的抽象方法实现,需要枚举类中的每个对象都对其进行实现。

4.2. 服务升级 - 反序列化失败

%%
▶3.🏡⭐️◼️【🌈费曼无敌🌈⭐️第一步⭐️】◼️⭐️-point-20230401-2133%%
❕ ^tai2qg

4.2.1. 序列化内容与代码中不一致

在系统或者类库升级时,对其中定义的枚举类型多加注意,为了保持代码上的兼容性,如果我们定义的枚举类型有可能会被序列化保存 (放到文件中、保存到数据库中,进入分布式内存缓存中),那么我们是不能够删除原来枚举类型中定义的任何枚举对象的,否则程序在运行过程中,JVM 拿着之前保存的名称进行反序列化时,就会抱怨找不到与某个名字对应的枚举对象了

4.2.2. 服务端与客户端不一致

另外,在远程方法调用过程中,如果我们发布的客户端接口返回值中使用了枚举类型,那么服务端在升级过程中就需要特别注意。如果在接口的返回结果的枚举类型中添加了新的枚举值,那就会导致仍然在使用老的客户端的那些应用出现调用失败的情况。
#todo

- [ ] 🚩 - 实战经验列表筛选规则定义 - 🏡 2023-02-09 09:30

5. 参考与感谢

https://www.cnblogs.com/z00377750/p/9177097.html
[[深度分析Java的枚举类型—-枚举的线程安全性及序列化问题 - 风动静泉 - 博客园]]