在单例模式中,我们会存在序列化破坏单例模式的问题,但是我们可以通过用枚举类保证。因为枚举类天生就是满足单例模式的。所以下面就总结一下序列化对于对象和枚举的关系。

枚举和对象的反序列化

我们主要使用ObjectInputStreamreadObject方法:

在源码中调用了Object obj = readObject0(type, false);来读取对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
...
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

readObject0,针对对象和枚举有不同的处理方式

  1. Object:checkResolve(readOrdinaryObject(unshared));
  2. Enum:checkResolve(readEnum(unshared));
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
private Object readObject0(Class<?> type, boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));

case TC_OBJECT:
if (type == String.class) {
throw new ClassCastException("Cannot cast an object to java.lang.String");
}
return checkResolve(readOrdinaryObject(unshared));

...

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

读取对象

在这里面,我们分两部分,一部分是反序列化对象,也就是obj = desc.isInstantiable() ? desc.newInstance() : null;如果我们对象可以实例化,那么我们这句话就会新建一个对象,这也就是为什么反序列化会破坏单例模式的原因,我们会新建一个对象,和原来存在的对象不是一个。但是还有另外的一部分,就是对对象中readResolve方法的判断:如果我们对象中含有desc.hasReadResolveMethod()也就是含有readResolve方法,那么我们就会调用这个方法获取他的返回值。如果不为空且和前面新建对象不同,那么我们就会返回这个方法返回的对象。这种方法可以保证单例模式

为什么重写resolve方法可以保证单例:可以理解成,我们把对象持久化的时候,对象内部的单例也被持久化了,我们resolve方法只要返回这个单例,就可以保证返回原来的对象。

在源码行Object rep = desc.invokeReadResolve(obj);中,desc是一个ObjectStreamClass的实例,它提供了关于正在处理的对象的类的元数据。invokeReadResolve方法调用了对象的readResolve方法,并获取其返回值。如果这个返回值与原始对象(obj)不同,Java序列化机制会用这个返回值来替换原始对象,从而维护了单例性(如果返回值是单例实例的话)。

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
37
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
...
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}

...

if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}

return obj;
}

读取枚举

书接上回,我们从针对枚举的读取流程来继续分析。调用了readEnum方法。

1
2
3
4
5
case TC_ENUM:
if (type == String.class) {
throw new ClassCastException("Cannot cast an enum to java.lang.String");
}
return checkResolve(readEnum(unshared));

我们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
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
...
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
// 根据名称来读取
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}

handles.finish(enumHandle);
passHandle = enumHandle;
return result;

下面讲解下枚举类序列化和反序列化的过程:

在Java中,枚举的序列化和反序列化处理有些特别,因为Java为枚举类型提供了特殊的支持,以确保它们在序列化和反序列化过程中保持唯一性。这种处理方式保证了枚举值在整个应用中是单例的,并且在网络传输或持久化存储中保持一致性。

枚举的序列化

当一个枚举实例被序列化时,Java序列化机制并不是将枚举实例作为一个普通对象来处理,而是特殊处理以确保只有枚举的名称(即枚举常量的名字)被序列化。枚举类型的所有其他状态信息,如字段等,都不会被序列化。这是因为每个枚举常量在JVM中都是唯一的,且在枚举类型定义时已经被创建。

具体来说,序列化系统会做以下几件事:

  1. 确定对象是枚举类型。
  2. 写入枚举的类型信息。
  3. 仅存储枚举常量的名称。

这样,序列化的输出非常简洁,仅包括足以唯一标识枚举值的最少量信息。

枚举的反序列化

在反序列化过程中,事情变得更加有趣。当枚举类型的数据从流中读取时,反序列化机制会:

  1. 读取并确定枚举的类型。
  2. 根据存储的名称找到对应的枚举常量。

这是通过调用Enum.valueOf()来实现的,这个方法将枚举类型和从序列化数据中读取的名字作为参数,并返回相应的枚举常量。这保证了枚举的单例性,因为这个方法总是返回枚举类型中预定义的相同实例。

代码示例

考虑以下枚举类型:

1
2
3
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}

当你序列化一个Day.MONDAY实例时,序列化机制只存储关于它是Day类型的MONDAY这一事实。当反序列化时,Enum.valueOf(Day.class, "MONDAY")被调用,返回Day类中的MONDAY常量。

优点

这种处理方式的优点包括:

  • 安全性:由于枚举实例在枚举类型定义时就被创建,反序列化不会创建新的实例,这避免了潜在的攻击向量,如通过序列化创建对象的实例来绕过常规的实例控制。
  • 性能:序列化和反序列化过程只处理枚举常量的名称,这使得这两个过程相对高效。
  • 简洁性:枚举的序列化形式非常紧凑,仅包含足够的信息来精确地识别特定的枚举常量。

枚举的这种特殊处理使其成为Java中表达固定集合元素的理想选择,并且在需要序列化支持的分布式系统中尤为有用。