在单例模式中,我们会存在序列化破坏单例模式的问题,但是我们可以通过用枚举类保证。因为枚举类天生就是满足单例模式的。所以下面就总结一下序列化对于对象和枚举的关系。
枚举和对象的反序列化 我们主要使用ObjectInputStream
的readObject
方法:
在源码中调用了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
,针对对象和枚举有不同的处理方式
Object:checkResolve(readOrdinaryObject(unshared));
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) { 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中都是唯一的,且在枚举类型定义时已经被创建。
具体来说,序列化系统会做以下几件事:
确定对象是枚举类型。
写入枚举的类型信息。
仅存储枚举常量的名称。
这样,序列化的输出非常简洁,仅包括足以唯一标识枚举值的最少量信息。
枚举的反序列化 在反序列化过程中,事情变得更加有趣。当枚举类型的数据从流中读取时,反序列化机制会:
读取并确定枚举的类型。
根据存储的名称找到对应的枚举常量。
这是通过调用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中表达固定集合元素的理想选择,并且在需要序列化支持的分布式系统中尤为有用。