如何将两个Java序列化对象重新链接在一起?

10
有时(实际上很多时候)我们会在Java中遇到两个对象指向同一个东西的情况。现在,如果我们将它们分别序列化,那么这些序列化形式应该有对象的独立副本,因为应该能够打开其中一个而不打开另一个。但是,如果我们现在对它们进行反序列化,我们发现它们仍然是分开的。是否有任何方法可以将它们重新连接起来?
以下是示例。
public class Example {

 private static class ContainerClass implements java.io.Serializable {
  private ReferencedClass obj;
  public ReferencedClass get() {
   return obj;
  }
  public void set(ReferencedClass obj) {
   this.obj = obj;
  }
 }

 private static class ReferencedClass implements java.io.Serializable {
  private int i = 0;
  public int get() {
   return i;
  }
  public void set(int i) {
   this.i = i;
  }
 }

 public static void main(String[] args) throws Exception {
  //Initialise the classes
  ContainerClass test1 = new ContainerClass();
  ContainerClass test2 = new ContainerClass();
  ReferencedClass ref = new ReferencedClass();

  //Make both container class point to the same reference
  test1.set(ref);
  test2.set(ref);

  //This does what we expect: setting the integer in one (way of accessing the) referenced class sets it in the other one
  test1.get().set(1234);
  System.out.println(Integer.toString(test2.get().get()));

  //Now serialise the container classes
  java.io.ObjectOutputStream os = new java.io.ObjectOutputStream(new java.io.FileOutputStream("C:\\Users\\Public\\test1.ser"));
  os.writeObject(test1);
  os.close();
  os = new java.io.ObjectOutputStream(new java.io.FileOutputStream("C:\\Users\\Public\\test2.ser"));
  os.writeObject(test2);
  os.close();

  //And deserialise them
  java.io.ObjectInputStream is = new java.io.ObjectInputStream(new java.io.FileInputStream("C:\\Users\\Public\\test1.ser"));
  ContainerClass test3 = (ContainerClass)is.readObject();
  is.close();
  is = new java.io.ObjectInputStream(new java.io.FileInputStream("C:\\Users\\Public\\test2.ser"));
  ContainerClass test4 = (ContainerClass)is.readObject();
  is.close();

  //We expect the same thing as before, and would expect a result of 4321, but this doesn't happen as the referenced objects are now separate instances
  test3.get().set(4321);
  System.out.println(Integer.toString(test4.get().get()));
 }

}

+1是在任何语言中反序列化对象的噩梦。 - Romain Hippeau
3个回答

3
readResolve()方法可以实现此功能(当然,首先您必须定义如何决定哪些对象是“相同的”)。但更简单的方法是将两个对象序列化到同一个文件中 - ObjectOut / InputStream会记录其已序列化/反序列化的所有对象,并且仅存储和返回它已经看到的对象的引用。

(也许包括writeReplace。) - Tom Hawtin - tackline
大致来说,我尝试这样做的原因是我有一个包含数据的文件,在应用程序的多个会话中不太可能更改,还有一个包含特定于应用程序单个会话的数据。它们之间具有交叉引用,但我希望保持它们在逻辑上分开。 - Adam Burley

1
像上面的答案一样,readResolve是关键,因为它允许您将“重复”的对象替换为您想要的对象。
假设您的类实现了hashCode()和equals(),您可以通过创建一个静态的WeakHashMap来实现去重,该映射将所有仍在内存中的已创建对象的引用保存在其中。 例如:
class ReferencedClass implements Serializable
{
   static private Map<ReferencedClass, Reference<ReferencedClass>> map = new WeakHashMap<ReferencedClass, Reference<ReferencedClass>>;

   static public ReferencedClass findOriginal(ReferencedClass obj)
   {
      WeakReference<ReferencedClass> originalRef = map.get(obj);
      ReferencedClass original = originalRef==null ? null : originalRef.get();
      if (original==null)
      {
          original = obj;
          map.put(original, new WeakReference<ReferencedClass>(original));
      }
      return original;
   }

   static public ReferencedClass()
   {
        findOriginal(this);
   }

   private Object readResolve()
   {
       return findOriginal(this);
   }
}

在反序列化时,readResolve() 调用 RerencedClass.findOriginal(this) 来获取当前原始实例。如果该类的实例仅通过反序列化创建,则这将按原样工作。如果您还使用 new 运算符构造对象,则您的构造函数也应调用 findOriginal,传递 this,以便这些对象也添加到池中。

有了这些更改,两个 ContainerClass 实例都将指向同一个 ReferenceClass 实例,即使它们是独立反序列化的。


1

我曾为正在构建的应用服务器/对象数据库做过类似的事情。您的要求是什么-为什么需要这样做?如果您的要求不到应用服务器的程度,那么可能其他设计更容易解决它。


如果您仍然想要继续,以下是操作步骤:

首先,您需要通过覆盖ObjectOutputStream.replaceObject()ObjectInputStream.resolveObject()方法来钩取序列化过程。请参考我的ObjectSerializer示例。

在序列化对象时,您必须为每个要具有唯一标识的对象实例分配一个唯一的ID - 这种类型的对象通常称为实体。当对象引用其他实体时,您必须使用包含所引用实体的ID的占位符对象替换这些其他实体。

然后当对象被反序列化时,您必须将每个占位符对象替换为具有该ID的真实实体对象。您需要跟踪已加载到内存中的实体对象实例及其ID,以便对于每个ID,只创建一个实例。如果实体尚未加载到内存中,则必须从保存它的位置加载它。请参见我的 EntityManager以获取示例。

如果你想进行懒加载,避免在不需要时将整个对象图加载到内存中,你必须做类似于透明引用的事情。请查看他们的实现这里。如果你已经做到了这一步,那么你可以从我的项目中复制这些部分(包括entitiesentities.trefserial和可能还有context),因为它具有宽松的许可证,并修改它们以适应你的需求(即删除你不需要的内容)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接