[英]Java serialization: readObject() vs. readResolve()
Effective Java一书和其他资源提供了关于在使用可序列化的 Java 类时如何以及何时使用readObject()
方法的很好的解释。 另一方面, readResolve()
方法仍然有点神秘。 基本上我发现的所有文件要么只提到两者之一,要么单独提到两者。
尚未回答的问题是:
readResolve()
,尤其是在返回什么方面?我希望你能对这件事有所了解。
readResolve
用于替换从流中读取的对象。 我见过的唯一用途是强制单身人士; 读取对象时,将其替换为单例实例。 这确保没有人可以通过序列化和反序列化单例来创建另一个实例。
当ObjectInputStream
从流中读取一个对象并准备将其返回给调用方时,将调用readResolve
方法。 ObjectInputStream
检查对象的类是否定义了readResolve
方法。 如果定义了该方法,则将调用readResolve
方法,以允许流中的对象指定要返回的对象。 返回的对象应该是与所有用途兼容的类型。 如果不兼容, ClassCastException
在发现类型不匹配时将引发ClassCastException
。
第 90 项,Effective Java,第 3 版介绍了串行代理的readResolve
和writeReplace
它们的主要用途。 这些示例没有写出readObject
和writeObject
方法,因为它们使用默认序列化来读取和写入字段。
readResolve
在readObject
返回后调用(相反, writeReplace
在writeObject
之前调用并且可能在不同的对象上调用)。 该方法返回的对象替换了返回给ObjectInputStream.readObject
用户的this
对象以及流中对该对象的任何进一步反向引用。 readResolve
和writeReplace
都可能返回相同或不同类型的对象。 在某些情况下,返回相同的类型很有用,其中字段必须是final
的并且需要向后兼容或必须复制和/或验证值。
使用readResolve
不会强制执行单例属性。
readResolve 可用于更改通过 readObject 方法序列化的数据。 例如,xstream API 使用此功能来初始化一些不在要反序列化的 XML 中的属性。
readObject()
是ObjectInputStream
类中的现有方法。 在反序列化时, readObject()
方法会在内部检查正在反序列化的对象是否已实现readResolve()
方法。 如果readResolve()
方法存在,那么它将被调用
示例readResolve()
实现如下所示
protected Object readResolve() {
return INSTANCE:
}
因此,编写readResolve()
方法的目的是确保返回 JVM 中的同一对象,而不是在反序列化期间创建新对象。
readResolve 适用于当您可能需要返回现有对象时,例如因为您正在检查应该合并的重复输入,或者(例如在最终一致的分布式系统中)因为它是一个可能在您意识到之前到达的更新任何旧版本。
正如已经回答的那样, readResolve
是在反序列化对象时在 ObjectInputStream 中使用的私有方法。 这是在返回实际实例之前调用的。 在单例的情况下,这里我们可以强制返回已经存在的单例实例引用而不是反序列化的实例引用。 类似地,我们有 ObjectOutputStream 的writeReplace
。
readResolve
示例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SingletonWithSerializable implements Serializable {
private static final long serialVersionUID = 1L;
public static final SingletonWithSerializable INSTANCE = new SingletonWithSerializable();
private SingletonWithSerializable() {
if (INSTANCE != null)
throw new RuntimeException("Singleton instance already exists!");
}
private Object readResolve() {
return INSTANCE;
}
public void leaveTheBuilding() {
System.out.println("SingletonWithPublicFinalField.leaveTheBuilding() called...");
}
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SingletonWithSerializable instance = SingletonWithSerializable.INSTANCE;
System.out.println("Before serialization: " + instance);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("file1.ser"))) {
out.writeObject(instance);
}
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("file1.ser"))) {
SingletonWithSerializable readObject = (SingletonWithSerializable) in.readObject();
System.out.println("After deserialization: " + readObject);
}
}
}
输出:
Before serialization: com.ej.item3.SingletonWithSerializable@7852e922
After deserialization: com.ej.item3.SingletonWithSerializable@7852e922
readResolve() 将在序列化时确保单例契约。
请参考
当使用序列化将一个对象转换为可以保存在文件中时,我们可以触发一个方法,readResolve()。 该方法是私有的,并保存在同一个类中,其对象在反序列化时被检索。 它确保在反序列化之后,返回的对象与序列化后的对象相同。 即instanceSer.hashCode() == instanceDeSer.hashCode()
readResolve() 方法不是静态方法。 在反序列化时调用in.readObject()
之后,它只是确保返回的对象与out.writeObject(instanceSer)
时如下序列化的对象相同
..
ObjectOutput out = new ObjectOutputStream(new FileOutputStream("file1.ser"));
out.writeObject(instanceSer);
out.close();
这样,它也有助于单例设计模式的实现,因为每次都返回相同的实例。
public static ABCSingleton getInstance(){
return ABCSingleton.instance; //instance is static
}
我知道这个问题真的很老并且有一个公认的答案,但是因为它在谷歌搜索中出现的频率很高我想我会权衡一下因为没有提供的答案涵盖我认为重要的三种情况 - 在我看来这些的主要用途方法。 当然,所有假设实际上都需要自定义序列化格式。
以集合类为例。 与仅按顺序序列化元素相比,链表或 BST 的默认序列化将导致空间的巨大损失,而性能增益却微乎其微。 如果集合是投影或视图,则更是如此 - 保留对比其公共 API 公开的更大结构的引用。
如果序列化对象具有需要自定义序列化的不可变字段,则writeObject/readObject
的原始解决方案是不够的,因为反序列化对象是在读取writeObject
中写入的流部分之前创建的。 以链表的这个最小实现为例:
public class List<E> extends Serializable { public final E head; public final List<E> tail; public List(E head, List<E> tail) { if (head==null) throw new IllegalArgumentException("null as a list element"); this.head = head; this.tail = tail; } //methods follow... }
这个结构可以通过递归地写入每个链接的head
字段,后跟一个null
值来序列化。 然而,反序列化这种格式变得不可能: readObject
无法更改成员字段的值(现在固定为null
)。 writeReplace
/ readResolve
对来了:
private Object writeReplace() {
return new Serializable() {
private transient List<E> contents = List.this;
private void writeObject(ObjectOutputStream oos) {
List<E> list = contents;
while (list!=null) {
oos.writeObject(list.head);
list = list.tail;
}
oos.writeObject(null);
}
private void readObject(ObjectInputStream ois) {
List<E> tail = null;
E head = ois.readObject();
if (head!=null) {
readObject(ois); //read the tail and assign it to this.contents
this.contents = new List<>(head, this.contents)
}
}
private Object readResolve() {
return this.contents;
}
}
}
如果上面的例子不能编译(或工作),我很抱歉,但希望它足以说明我的观点。 如果您认为这是一个非常牵强的示例,请记住许多函数式语言在 JVM 上运行,并且这种方法在它们的情况下变得必不可少。
我们可能想要实际反序列化一个与我们写入ObjectOutputStream
不同的类的对象。 对于诸如java.util.List
列表实现之类的视图,它会公开来自较长ArrayList
的切片。 显然,序列化整个后备列表不是一个好主意,我们应该只写入查看切片中的元素。 但是,为什么在反序列化后停止使用它并具有无用的间接级别? 我们可以简单地将流中的元素读入ArrayList
并直接返回它,而不是将其包装在我们的视图类中。
或者,具有专用于序列化的类似委托类可能是一种设计选择。 一个很好的例子是重用我们的序列化代码。 例如,如果我们有一个构建器类(类似于 String 的 StringBuilder),我们可以编写一个序列化委托,它通过向流写入一个空构建器来序列化任何集合,然后是集合大小和集合迭代器返回的元素。 反序列化将涉及读取构建器,附加所有后续读取的元素,并从委托readResolve
返回最终build()
的结果。 在那种情况下,我们只需要在集合层次结构的根类中实现序列化,并且不需要当前或未来实现的额外代码,前提是它们实现了抽象iterator()
和builder()
方法(后者用于重新创建相同类型的集合——这本身就是一个非常有用的特性)。 另一个例子是有一个我们不能完全控制代码的类层次结构——我们来自第三方库的基类可能有任意数量的我们一无所知的私有字段,并且可能从一个版本更改为另一个版本,破坏我们的序列化对象。 在这种情况下,在反序列化时手动写入数据并重建对象会更安全。
读取解决方法
对于 Serializable 和 Externalizable 类,readResolve 方法允许类在返回给调用者之前替换/解析从流中读取的对象。 通过实现readResolve方法,一个类可以直接控制自己被反序列化的实例的类型和实例。 该方法定义如下:
ANY-ACCESS-MODIFIER 对象 readResolve() 抛出 ObjectStreamException;
当ObjectInputStream从流中读取一个对象并准备将它返回给调用者时,将调用readResolve方法。 ObjectInputStream检查对象的类是否定义了 readResolve 方法。 如果定义了该方法,则调用readResolve方法让流中的对象指定要返回的对象。 返回的对象应该是与所有用途兼容的类型。 如果不兼容,发现类型不匹配时会抛出ClassCastException 。
例如,可以创建一个Symbol类,每个符号绑定在虚拟机中只存在一个实例。 将实施readResolve方法以确定该符号是否已定义并替换先前存在的等效 Symbol 对象以维护标识约束。 通过这种方式,可以在序列化过程中保持 Symbol 对象的唯一性。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.