使用Set面向循环依赖时的Java序列化错误

我的项目是使用Hibernate和Weblogic服务器的EJB3java项目。

为了方便起见(据我所知,这是典型的hibernate ),一些实体包含循环依赖(父知道孩子,孩子知道父母)。 此外,对于某些子类 – hashCode()equals()方法依赖于它们的父类(因为它是唯一键)。

在工作时我看到了一个奇怪的行为 – 从服务器返回到客户端的一些集合虽然包含正确的元素,但它们的行为却没有包含任何内容。 例如,一个简单的测试,例如: set.contains(set.toArray()[0])返回false尽管hashCode()方法是一个很好的方法。

经过大量的调试后,我能够生成2个简单的类来重现问题(我可以向你保证,两个类中的hashCode()函数都是自反的,传递的和对称的 ):

 package test; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.HashSet; import java.util.Set; public class ClientTest implements Serializable { public static void main(String[] args) throws Exception { SerializableClass serializationTest = new SerializableClass(); FieldOfSerializableClass hashMember = new FieldOfSerializableClass(); hashMember.setParentLink(serializationTest); serializationTest.setHashCodeField("Some string"); serializationTest .setSomeSet(new HashSet()); serializationTest.getSomeSet().add(hashMember); System.out.println("Does it contain its member? (should return true!) " + serializationTest.getSomeSet().contains(hashMember)); new ObjectOutputStream(new FileOutputStream("temp")) .writeObject(serializationTest); SerializableClass testAfterDeserialize = (SerializableClass) new ObjectInputStream( new FileInputStream(new File("temp"))).readObject(); System.out.println("Does it contain its member? (should return true!) " + testAfterDeserialize.getSomeSet().contains(hashMember)); for (Object o : testAfterDeserialize.getSomeSet()) { System.out.println("Does it contain its member by equality? (should return true!) "+ o.equals(hashMember)); } } public static class SerializableClass implements Serializable { private Set mSomeSet; private String mHashCodeField; public void setSomeSet(Set pSomeSet) { mSomeSet = pSomeSet; } public Set getSomeSet() { return mSomeSet; } public void setHashCodeField(String pHashCodeField) { mHashCodeField = pHashCodeField; } @Override public int hashCode() { final int prime = 31; int result = 1; System.out.println("In hashCode - value of mHashCodeField: " + mHashCodeField); result = prime * result + ((mHashCodeField == null) ? 0 : mHashCodeField.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SerializableClass other = (SerializableClass) obj; if (mHashCodeField == null) { if (other.mHashCodeField != null) { return false; } } else if (!mHashCodeField.equals(other.mHashCodeField)) return false; return true; } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("Just started serializing"); in.defaultReadObject(); System.out.println("Just finished serializing"); } } public static class FieldOfSerializableClass implements Serializable { private SerializableClass mParentLink; public void setParentLink(SerializableClass pParentLink) { mParentLink = pParentLink; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mParentLink == null) ? 0 : mParentLink.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; FieldOfSerializableClass other = (FieldOfSerializableClass) obj; if (mParentLink == null) { if (other.mParentLink != null) { return false; } } else if (!mParentLink.equals(other.mParentLink)) return false; return true; } } } 

这产生了以下输出:

    在hashCode中 -  mHashCodeField的值:一些字符串
    在hashCode中 -  mHashCodeField的值:一些字符串
    它是否包含其成员?  (应该返回true!)true
    刚开始序列化
    在hashCode中 -  mHashCodeField的值:null
    刚刚完成序列化
    在hashCode中 -  mHashCodeField的值:一些字符串
    它是否包含其成员?  (应该返回true!)false
    它是否通过平等包含其成员?  (应该返回true!)true

这告诉我Java序列化对象的顺序是错误的! 它开始序列化字符串之前的Set,从而导致上述问题。

在这种情况下我该怎么办? 有没有选择(除了为许多实体实现readResolve …)以指导java按特定顺序序列化一个类? 另外,实体将hashCode在其父级上是否根本错误?

编辑 :一位同事提出了一个解决方案 – 因为我正在使用Hibernate,所以每个实体都有一个唯一的长ID。 我知道Hibernate指定不在equals方法中使用此ID – 但是hashCode呢? 使用此唯一ID作为哈希码似乎可以解决上述问题,并将性能问题的风险降至最低。 使用ID作为哈希码是否有任何其他影响?

第二次编辑 :我去实现了我的部分解决方案(现在所有的enteties都使用了hashCode()函数的ID字段,并且不再继续使用其他的enteties)但是,唉,序列化错误仍然困扰着我! 下面是另一个序列化错误的示例代码。 我认为正在发生的是 – ClassA开始反序列化,看到它有一个ClassB来反序列化,并且在它反序列化它的ID之前,它开始反序列化ClassB。 B开始反序列化并且看到它有一组ClassA。 ClassA实例是部分反序列化的,但即使ClassB将它添加到Set(使用ClassA的缺失ID),完成deserializning,然后ClassA完成并发生错误。

我该怎么做才能解决这个问题?! 循环依赖是Hibernate中一个非常常用的实践,我不能接受它,我是唯一有这个问题的人。

另一种可能的解决方案是为hashCode设置一个专用变量(将由对象的ID计算)并确保(查看readObject和writeObject)它将在非常其他对象之前读取。 你怎么看? 这个解决方案有什么缺点吗?

示例代码:

 import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.HashSet; import java.util.Set; public class Test implements Serializable { public static void main(String[] args) throws Exception { ClassA aClass = new ClassA(); aClass.setId(Long.valueOf(321)); ClassB bClass = new ClassB(); bClass.setId(Long.valueOf(921)); Set set = new HashSet(); set.add(aClass); bClass.setSetfield(set); aClass.setBField(bClass); Set goodClassA = aClass.getBField().getSetfield(); Set badClassA = serializeAndDeserialize(aClass).getBField().getSetfield(); System.out.println("Does it contain its member? (should return true!) " + goodClassA.contains(goodClassA.toArray()[0])); System.out.println("Does it contain its member? (should return true!) " + badClassA.contains(badClassA.toArray()[0])); } public static ClassA serializeAndDeserialize(ClassA s) throws Exception { new ObjectOutputStream(new FileOutputStream(new File("temp"))).writeObject(s); return (ClassA) new ObjectInputStream(new FileInputStream(new File("temp"))).readObject(); } public static class ClassB implements Serializable { private Long mId; private Set mSetfield = new HashSet(); public Long getmId() { return mId; } public void setId(Long mId) { this.mId = mId; } public Set getSetfield() { return mSetfield; } public void setSetfield(Set mSetfield) { this.mSetfield = mSetfield; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mId == null) ? 0 : mId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ClassB other = (ClassB) obj; if (mId == null) { if (other.mId != null) return false; } else if (!mId.equals(other.mId)) return false; return true; } } public static class ClassA implements Serializable { private Long mId; private ClassB mBField; public Long getmId() { return mId; } public void setId(Long mId) { this.mId = mId; } public ClassB getBField() { return mBField; } public void setBField(ClassB mBField) { this.mBField = mBField; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mId == null) ? 0 : mId.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ClassA other = (ClassA) obj; if (mId == null) { if (other.mId != null) return false; } else if (!mId.equals(other.mId)) return false; return true; } } } 

因此,当我阅读它时,您将基于父对象的FieldOfSerializableClass的hashCode。 这似乎是您的问题的最终原因和一个非常有问题的设计。 hashCode()equals()方法处理对象标识,并且根本不应该与父包含它们的内容相关。 一个对象的身份根据哪个父对象拥有它而改变的想法至少对我来说是非常陌生的,并且是你的代码不起作用的最终原因。

虽然其他答案有一些解决问题的方法,但我认为解决这个问题的最简单方法是给FieldOfSerializableClass类一个自己的标识。 您可以将mHashCodeFieldSerializableClass复制到FieldOfSerializableClass 。 在对象上设置父级时,可以使用其mHashCodeField并将其存储在本地。

 public void setParentLink(SerializableClass pParentLink) { this.mHashCodeField = pParentLink.mHashCodeField; mParentLink = pParentLink; } 

然后hashcode(和equals)方法看起来类似于SerializableClass

 @Override public int hashCode() { return ((mHashCodeField == null) ? 0 : mHashCodeField.hashCode()); } 

但实际上你应该考虑改变代码,以便父母关系更少耦合。 考虑一下如果在某个字段上调用setParentLink()而它已经在另一个SerializableClass集中时会发生什么。 突然之间,原始类甚至无法在其集合中找到该项目,因为其标识已更改。 将一些排序标识分配给FieldOfSerializableClass类,该类是父类中唯一的,就Java对象而言,这是最好的模式。

如果不能将UUID.randomUUID()的其他字段用作正确的标识,则可以在每次提供新id的类上使用UUID.randomUUID()或一些静态AtomicInteger 。 但是我会使用Hibernate给你的自动生成的id。 您只需确保在将对象放入另一个对象的集合之前已将该对象插入到数据库中。

Deserailization将两个字段( mHashCodeFieldmSomeSet )的值读取到临时数组中,并且在对这两个值进行反序列化后,它将字段设置为存储的值。

由于HashSet在反序列化期间重新计算其元素的哈希码,因此当它仍为空时将使用mHashCodeField

可能的解决方案是将mSomeSet标记为瞬态,并在writeObject / readObject中写入/读取它。

 @SuppressWarnings("unchecked") private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("Just started deserializing"); in.defaultReadObject(); mSomeSet=(Set)in.readObject(); System.out.println("Just finished deserializing"); } private void writeObject(java.io.ObjectOutputStream out) throws IOException { System.out.println("Just started serializing"); out.defaultWriteObject(); out.writeObject(mSomeSet); System.out.println("Just finished serializing"); } 

这是等于方法必须是反身的,传递的和对称的……

hashCode方法必须具有以下属性 :

hashCode的一般契约是:

每当在执行Java应用程序期间多次在同一对象上调用它时,hashCode方法必须始终返回相同的整数,前提是不修改对象上的equals比较中使用的信息。 从应用程序的一次执行到同一应用程序的另一次执行,该整数不需要保持一致。

如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须生成相同的整数结果。

如果两个对象根据equals(java.lang.Object)方法不相等,则不需要在两个对象中的每一个上调用hashCode方法必须生成不同的整数结果。 但是,程序员应该知道为不等对象生成不同的整数结果可能会提高哈希表的性能。

在这里,看起来用于在反序列化期间将条目放入集合中的hashCode与在contains()期间计算的条目不同。 顺便说一句,你注意到条目在Set中你只是无法通过它的hashCode访问它,如果你循环遍历Set的内容你将找到元素。

可能的解决方案:

  • 有一个不依赖于父对象的hashCode。
  • 使用不使用哈希码的数据结构(List,TreeSet ……)
  • 不要在Set上使用contains方法…
  • 实现ReadResolve以在desirialization后重新创建Set …

[编辑]:看起来你并不孤单bug_id = 4957674

我添加了另一个答案,因为它与我的第一个非常不同:

这是一个没有瞬态字段的实现,我在这里找到了必要的信息: 高级序列化和这里 。

顺便说一下,我也尝试使用serialPersistentFields属性来强制mHashCodeFields先被序列化,但它没有帮助……

  public static class SerializableClass implements Serializable { // this tells the serialization mechanism to serialize only mHasCodeField... private final static ObjectStreamField[] serialPersistentFields = { new ObjectStreamField( "mHashCodeField", String.class) }; private String mHashCodeField; private Set mSomeSet; public void setSomeSet(Set pSomeSet) { mSomeSet = pSomeSet; } public Set getSomeSet() { return mSomeSet; } public void setHashCodeField(String pHashCodeField) { mHashCodeField = pHashCodeField; } @Override public int hashCode() { final int prime = 31; int result = 1; System.out.println("In hashCode - value of mHashCodeField: " + mHashCodeField); result = prime * result + ((mHashCodeField == null) ? 0 : mHashCodeField.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; SerializableClass other = (SerializableClass) obj; if (mHashCodeField == null) { if (other.mHashCodeField != null) { return false; } } else if (!mHashCodeField.equals(other.mHashCodeField)) return false; return true; } private void writeObject(java.io.ObjectOutputStream out) throws IOException, ClassNotFoundException { System.out.println("Just started serializing"); out.defaultWriteObject(); out.writeObject(mSomeSet); System.out.println("In writeObject - value of mHashCodeField: " + mHashCodeField); System.out.println("Just finished serializing"); } private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("Just started deserializing"); in.defaultReadObject(); mSomeSet=(Set)in.readObject(); System.out.println("In readObject - value of mHashCodeField: " + mHashCodeField); System.out.println("Just finished deserializing"); } } 

事实上,Hibernate说不要将id用作哈希码,但我相信它们对它过于严格。 只有当id由Hibernate自动生成/自动增量时才有意义。 在这种情况下,您可能只有在Hibernate决定将其实际持久化到数据库时才接收其id值的bean,因此在这种情况下,您可能会从使用该ID的哈希码和/或equals方法中获得不可预测的行为。 但是,如果id是手动设置的,即你的应用程序处理填充此值,那么我相信在你的hashcode / equals方法中使用它是完全可以的。 这是你的情况吗?

在我看来这是java中的错误,而不是您的源代码。 虽然上面的答案给出了很好的解决方法选项,但最好的解决方案是让Java修复反序列化如何解决循环引用和集合/散列映射的问题。

请参阅此处以制作新的错误报告: http : //bugreport.sun.com/bugreport/

报告此错误的人越多,他们修复错误的可能性就越大。 我在项目中也遇到了这个错误,而且解决方法比我们的价值要大得多。

此外,我发现了类似的错误报告: http : //bugs.sun.com/view_bug.do; jsessionid = fb27da16bb769ffffffffebce29d31b2574e?bug_id = 6208166

我遇到了同样的问题。 我认为你在关于原因的第二次编辑中是对的。 这是我对问题的最简单复制:

 public class Test { static class Thing implements Serializable { String name; Set others = new HashSet(); @Override public int hashCode() { if (name == null) { System.out.println("hashcode called with null name!"); } return name == null ? 0 : name.hashCode(); } @Override public boolean equals(Object o) { return o instanceof Thing && ((Thing) o).name == name; } } @org.junit.Test public void testHashSetCircularDependencySerialization() throws Exception { Thing thing = new Thing(); thing.name = "thing"; Thing thing2 = new Thing(); thing2.name = "thing2"; thing.others.add(thing2); thing2.others.add(thing); assertTrue(thing2.others.contains(thing)); Thing thingCopy = (Thing) serializeAndDeserialize(thing); Thing thing2Copy = thingCopy.others.iterator().next(); assertTrue(thing2Copy.others.contains(thingCopy)); } public static Object serializeAndDeserialize(Object other) throws Exception { ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); new ObjectOutputStream(byteOutputStream).writeObject(other); ByteArrayInputStream byteInputStream = new ByteArrayInputStream(byteOutputStream.toByteArray()); return new ObjectInputStream(byteInputStream).readObject(); } } 

输出:

 hashcode called with null name! 

此测试失败。 我发现最简单的解决方案是保留哈希码的副本。 因为它是一个主要的,它在反序列化期间初始化对象时设置,而不是以后:

  int hashcode; @Override public int hashCode() { if (hashcode != 0) { return hashcode; } hashcode = name == null ? 0 : name.hashCode(); return hashcode; } 

测试现在通过。