Java序列化:readObject()与readResolve()


127

有效的Java》和其他资源这本书对在处理可序列化Java类时如何以及何时使用readObject()方法提供了很好的解释。另一方面,readResolve()方法仍然是一个谜。基本上,我发现的所有文档都只提到了两者之一,或者只单独提到了两者。

仍未解决的问题是:

  • 两种方法有什么区别?
  • 什么时候应该实施哪种方法?
  • 应该如何使用readResolve(),尤其是在返回什么方面?

希望您能对此事有所了解。


Oracle JDK的示例:String.CaseInsensitiveComparator.readResolve()
kevinarpe '18年

Answers:


137

readResolve用于替换从流中读取的对象。我见过的唯一用途是强制单例。读取对象时,将其替换为单例实例。这样可以确保没有人可以通过对单例进行序列化和反序列化来创建另一个实例。


3
恶意代码(甚至数据)有多种解决方法。
Tom Hawtin-大头钉

6
乔什·布洛赫(Josh Bloch)讨论了在有效的Java第二版中打破这种情况的条件。项目77.他在几年前在Google IO上的一次演讲中提到了这一点(有时在演讲结束时):youtube.com/watch?
v=pi_I7oD_uGI

17
我发现这个答案有点不足,因为它没有提到transient字段。readResolve用于在读取对象后解析该对象。一个示例用法可能是对象拥有一些可以从现有数据重新创建的缓存,而无需序列化;可以声明缓存的数据,transientreadResolve()可以在反序列化之后对其进行重建。这样的事情就是这种方法的目的。
杰森C

2
@JasonC您的评论“类似[瞬态处理]的东西就是此方法的目的 ”是令人误解的。请参阅Java文档Serializable:它说:“ 从流中读取实例时需要指定替换的类应实现此[ readResolve]特殊方法...”。
奥弗

2
readResolve方法也可以用在极端情况下,假设您已经序列化了许多对象并将它们存储在数据库中。如果以后要将该数据迁移为新格式,则可以在readResolve方法中轻松实现。
Nilesh Rajani

29

项目90,有效的Java,第三版盖readResolvewriteReplace串行代理-它们的主要用途。这些示例未写出readObjectwriteObject方法,因为它们使用默认的序列化来读取和写入字段。

readResolvereadObject返回之后调用(相反writeReplace,在之前调用writeObject,可能在另一个对象上调用)。方法返回的对象将替换this返回给用户ObjectInputStream.readObject的对象以及流中对该对象的任何其他向后引用。双方readResolvewriteReplace可以返还相同或不同类型的对象。在某些情况下必须返回字段,并且必须final向后兼容,或者必须复制和/或验证值,在这种情况下,返回相同类型很有用。

用于 readResolve不会强制执行singleton属性。


9

readResolve可用于更改通过readObject方法序列化的数据。例如,xstream API使用此功能来初始化一些未反序列化的XML中的属性。

http://x-stream.github.io/faq.html#Serialization


1
XML和Xstream与有关Java序列化的问题无关,并且该问题早在几年前就已得到正确回答。-1
洛恩侯爵

5
接受的答案表明readResolve用于替换对象。该答案提供了有用的附加信息,可用于在反序列化期间修改对象。XStream仅作为示例给出,而不是唯一发生这种情况的库。

5

readResolve适用于您可能需要返回现有对象的情况,例如,因为您正在检查应合并的重复输入,或者(例如,在最终一致的分布式系统中)因为它是一个更新,可能在您意识到之前就已到达任何旧版本。


readResolve()对我来说很清楚,但我仍然
想着

5

readObject()是ObjectInputStream中的现有方法类中反序列化时读取对象时,readObject方法将在内部检查要反序列化的类对象是否具有readResolve方法,如果存在readResolve方法,则它将调用readResolve方法并返回该方法。实例。

因此,编写readResolve方法的意图是实现纯单例设计模式的好习惯,在这种模式下,没人可以通过序列化/反序列化获得另一个实例。



2

当使用序列化来转换对象以便可以将其保存在文件中时,我们可以触发一个方法readResolve()。该方法是私有的,并保留在反序列化过程中要检索其对象的同一类中。它确保反序列化之后,返回的对象与序列化的对象相同。那是,instanceSer.hashCode() == instanceDeSer.hashCode()

readResolve()方法不是静态方法。在in.readObject()反序列化之后调用after只是确保返回的对象与如下序列化的对象相同,而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 
}

1

我知道这个问题确实很老而且有一个可以接受的答案,但是当它在Google搜索中弹出时,我认为我很在意,因为没有提供的答案涵盖了我认为重要的三种情况-在我看来,这是主要的用法方法。当然,所有人都认为实际上需要自定义序列化格式。

以收集类为例。与仅按顺序序列化元素相比,链表或BST的默认序列化将导致巨大的空间损失,而性能提升却很小。如果集合是投影或视图,则更是如此-保留对大于其公共API所公开结构的引用。

  1. 如果序列化的对象具有需要自定义序列化的不可变字段,则原始的解决方案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上运行,这种情况下必不可少。

  1. 我们可能实际上想反序列化与我们写给的类不同的对象ObjectOutputStreamjava.util.List列表实现之类的视图就是这种情况,它从更长的列表中公开了一个切片ArrayList。显然,序列化整个后备列表是一个坏主意,我们应该只从查看的切片中写入元素。然而,为什么要停止它并在反序列化之后却有无用的间接级别?我们可以简单地将流中的元素读入an ArrayList并直接将其返回,而不用将其包装在我们的视图类中。

  2. 可替代地,具有专用于序列化的类似委托类可以是一种设计选择。一个很好的例子是重用我们的序列化代码。例如,如果我们有一个生成器类(类似于String的StringBuilder),我们可以编写一个序列化委托,该序列化是通过将一个空的生成器写入流中,然后集合的大小和由集合的迭代器返回的元素来序列化任何集合。反序列化将涉及读取构建器,附加所有随后读取的元素以及build()从委托返回final的结果readResolve。在那种情况下,我们只需要在集合层次结构的根类中实现序列化,并且如果当前或将来的实现实现了抽象,则不需要其他代码。iterator()builder()方法(后者用于重新创建相同类型的集合-这本身就是一个非常有用的功能)。另一个示例是具有一个类层次结构,该类层次结构我们无法完全控制代码-第三方库中的基类可能具有任意数量的私有字段,而这些私有字段我们一无所知,并且可能从一个版本更改为另一个版本,我们的序列化对象。在这种情况下,写数据并在反序列化时手动重建对象会更安全。


0

readResolve方法

对于Serializable和Externalizable类,readResolve方法允许类替换/解析从流中读取的对象,然后将其返回给调用方。通过实现readResolve方法,类可以直接控制要反序列化的自身实例的类型和实例。该方法定义如下:

ANY-ACCESS-MODIFIER对象readResolve()引发ObjectStreamException;

所述的readResolve当方法被调用ObjectInputStream的已读取来自流的对象,并准备将其返回给调用者。ObjectInputStream检查对象的类是否定义了readResolve方法。如果定义了该方法,则将调用readResolve方法以允许流中的对象指定要返回的对象。返回的对象应该是与所有用途兼容的类型。如果不兼容,在发现类型不匹配时将引发ClassCastException

例如,可以创建一个Symbol类,对于该类,在虚拟机中仅存在每个符号绑定的单个实例。所述的readResolve方法将实施,以确定该符号已经定义,并替换先前存在的等效Symbol对象保持标识约束。这样,可以在整个序列化过程中保持Symbol对象的唯一性。


0

正如已经回答的那样,readResolve是在反序列化对象时在ObjectInputStream中使用的私有方法。这在实际实例返回之前被调用。对于Singleton,在这里我们可以强制返回已经存在的Singleton实例引用,而不是反序列化实例引用。我们writeReplace对ObjectOutputStream 有类似的看法。

示例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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.