【问题标题】:Java serialization: readObject() vs. readResolve()Java 序列化:readObject() 与 readResolve()
【发布时间】:2010-11-13 04:36:23
【问题描述】:

Effective Java 一书和其他资源很好地解释了在处理可序列化 Java 类时如何以及何时使用 readObject() 方法。另一方面,readResolve() 方法仍然有点神秘。基本上我发现的所有文件要么只提到两者中的一个,要么只单独提到两者。

仍未解决的问题是:

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

希望你能对这件事有所了解。

【问题讨论】:

  • Oracle JDK 中的示例:String.CaseInsensitiveComparator.readResolve()

标签: java serialization singleton


【解决方案1】:

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

【讨论】:

  • 恶意代码(甚至数据)有很多方法可以绕过这个问题。
  • Josh Bloch 谈到了在有效的 Java 第 2 版中出现这种情况的条件。第 77 项。他在几年前在 Google IO 上发表的演讲中提到了这一点(有时在演讲结束时):youtube.com/watch?v=pi_I7oD_uGI
  • 我觉得这个答案有点不足,因为它没有提到transient 字段。 readResolve 用于在读取对象后解析对象。一个示例使用可能是一个对象拥有一些缓存,可以从现有数据重新创建,并且不需要序列化;缓存的数据可以声明为transientreadResolve()可以在反序列化后重建。类似的事情就是这种方法的用途。
  • @JasonC 你的评论“像[瞬态处理] 是这种方法用于”的评论具有误导性。请参阅Serializable 的 Java 文档:它说“当从流中读取实例时需要指定 replacement 的类应实现此 [readResolve] 特殊方法...” .
  • readResolve 方法也可以在极端情况下使用,假设您已经序列化了很多对象并将它们存储在数据库中。如果稍后您想将该数据迁移到新格式,您可以在 readResolve 方法中轻松实现。
【解决方案2】:

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

readResolvereadObject 返回之后被调用(相反writeReplacewriteObject 之前被调用并且可能在不同的对象上)。该方法返回的对象替换了返回给ObjectInputStream.readObject 用户的this 对象以及流中对该对象的任何进一步的反向引用。 readResolvewriteReplace 都可能返回相同或不同类型的对象。在字段必须为 final 并且需要向后兼容或必须复制和/或验证值的某些情况下,返回相同的类型很有用。

使用readResolve 不会强制执行单例属性。

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

    • XML 和 Xstream 与有关 Java 序列化的问题无关,并且该问题在几年前就已正确回答。 -1
    • 接受的答案指出 readResolve 用于替换对象。此答案提供了有用的附加信息,可用于在反序列化期间修改对象。 XStream 是作为示例给出的,而不是作为唯一可能发生这种情况的库。
    【解决方案4】:

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

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • readResolve() 对我来说很清楚,但我仍然有一些无法解释的问题,但你的回答只是读懂了我的想法,谢谢
      【解决方案6】:

      readResolve() 将在序列化时确保单例合约。
      refer

      【讨论】:

        【解决方案7】:

        当使用序列化将对象转换为可以保存在文件中时,我们可以触发一个方法,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 
        }
        

        【讨论】:

          【解决方案8】:

          正如已经回答的那样,readResolve 是反序列化对象时在 ObjectInputStream 中使用的私有方法。这是在实际实例返回之前调用的。在 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
          

          【讨论】:

            【解决方案9】:

            我知道这个问题真的很老并且有一个可以接受的答案,但由于它在谷歌搜索中弹出的频率很高,我想我会权衡一下,因为没有提供的答案涵盖我认为重要的三个案例 - 在我看来,主要用于这些方法。当然,都假设实际上需要自定义序列化格式。

            以集合类为例。与仅按顺序序列化元素相比,链表或 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. 我们可能想要实际反序列化一个不同于我们写入ObjectOutputStream 的类的对象。对于诸如java.util.List 列表实现之类的视图来说就是这种情况,它从更长的ArrayList 中公开一个切片。显然,序列化整个后备列表是一个坏主意,我们应该只写入查看切片中的元素。但是,为什么要停下来并在反序列化后有一个无用的间接级别?我们可以简单地将流中的元素读入ArrayList 并直接返回,而不是将其包装在我们的视图类中。

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

            【讨论】:

              【解决方案10】:

              readResolve 方法

              对于 Serializable 和 Externalizable 类,readResolve 方法允许类在返回给调用者之前替换/解析从流中读取的对象。通过实现 readResolve 方法,一个类可以直接控制自己被反序列化的实例的类型和实例。方法定义如下:

              ANY-ACCESS-MODIFIER 对象 readResolve() 抛出 ObjectStreamException;

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

              例如,可以创建一个 Symbol 类,其中每个符号绑定的单个实例仅存在于虚拟机中。 readResolve 方法将被实现以确定该符号是否已定义并替换先前存在的等效符号对象以维护标识约束。通过这种方式,可以跨序列化保持 Symbol 对象的唯一性。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-08-18
                • 2011-08-20
                • 2017-07-16
                • 2013-01-22
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多