Java serialization error when encountering a circular dependency with a set

My project is a java project on top of EJB3 using the Hibernate server and Weblogic .

For convenience (and, as I understand it, typical of hibernate ), some of the entities contain circular dependency (the parent knows that the child, the child knows the parent). In addition, for some child classes, the hashCode() and equals() method depends on their parent (since this is a unique key).

At work, I saw strange behavior. Some of the sets that were returned from the server to the client, although they contained the correct elements, acted as if they did not contain any. For example, a simple test, for example: set.contains(set.toArray()[0]) returned false , although the hashCode() method is good.

After extensive debugging, I was able to create 2 simple classes that reproduce the problem (I can assure that the hashCode() function in both classes is reflective, transitive, and symmetric ):

 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<FieldOfSerializableClass>()); 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<FieldOfSerializableClass> mSomeSet; private String mHashCodeField; public void setSomeSet(Set<FieldOfSerializableClass> pSomeSet) { mSomeSet = pSomeSet; } public Set<FieldOfSerializableClass> 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; } } } 

This produced the following conclusion:

  In hashCode - value of mHashCodeField: Some string
     In hashCode - value of mHashCodeField: Some string
     Does it contain its member?  (should return true!) true
     Just started serializing
     In hashCode - value of mHashCodeField: null
     Just finished serializing
     In hashCode - value of mHashCodeField: Some string
     Does it contain its member?  (should return true!) false
     Does it contain its member by equality?  (should return true!) true

This tells me that the order in which Java serializes the object is wrong! It starts the serialization of Set before String and thus causes the above problem.

What should I do in this situation? Is there any option (other than implementing readResolve for many objects ...) to direct java to serialize the class in a specific order? Also, is it fundamentally wrong that an entity bases its hashCode on its parent?

Change The solution was proposed by a colleague. Since I use Hibernate, each object has a unique long identifier. I know that Hibernate tells you not to use this identifier in the equals method, but what about hashCode? Using this unique identifier as hashcode seems to solve the above problem with minimal risk of performance problems. Are there any other consequences for using ID as hashcode?

SECOND EDITING : I went and implemented my partial solution (now all enteties use the ID field for the hashCode () function and no longer relay to other operations for it), but, alas, serialization errors still amaze me! The following is sample code with a different serialization error. I think this is happening: ClassA begins to deserialize, sees that the ClassB class has deserialization, and before deserializing its identifier, it starts to deserialize ClassB. B begins to deserialize and sees that he has a set of ClassA. A ClassA instance is partially deserialized, but even if ClassB adds it to Set (using the missing ClassA identifier), finishes deserializing, ClassA quits, and an error occurs.

What can I do to solve this ?! Looping dependencies is a very practical practice at Hibernate, and I just can't accept that I'm the only one with this problem.

Another possible solution is to have a dedicated variable for hashCode (it will be calculated by the identifier of the object) and make sure (view readObject and writeObject) that it will be read BEFORE a VERY OTHER OBJECT. What do you think? Are there any flaws in this solution?

Code example:

 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<ClassA> set = new HashSet<ClassA>(); set.add(aClass); bClass.setSetfield(set); aClass.setBField(bClass); Set<ClassA> goodClassA = aClass.getBField().getSetfield(); Set<ClassA> 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<ClassA> mSetfield = new HashSet<ClassA>(); public Long getmId() { return mId; } public void setId(Long mId) { this.mId = mId; } public Set<ClassA> getSetfield() { return mSetfield; } public void setSetfield(Set<ClassA> 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; } } } 
+7
source share
7 answers

So, when I read it, you base hashCode FieldOfSerializableClass on the parent object. This is apparently the main cause of your problem and a very dubious design. hashCode() and equals() refer to the identifier of the object and should not be related to what their parent contains. The idea that the identity of an object changes depending on which parent object owns it is at least foreign to me and is the main reason why your code doesn't work.

Although other answers have some ways around the problem, I think the easiest way to fix this is to give the FieldOfSerializableClass its own identity. You can copy mHashCodeField from SerializableClass to FieldOfSerializableClass . When the parent object is set to the object, you can take it mHashCodeField and save it locally.

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

Then the hashcode (and equals) method looks the same as for the SerializableClass .

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

But in fact, you should consider changing the code so that the parent relationship is less connected. Consider the second thing that happens if you call setParentLink() on a field while it is already in another SerializableClass . Suddenly, the original class cannot even find an element in its set, as its identity has changed. Assigning some view identifier to the FieldOfSerializableClass class, which is unique from the parent class, is the best example here in terms of Java objects.

You can use UUID.randomUUID() or some static AtomicInteger in the class, which each time gives a new identifier if you cannot use other fields in FieldOfSerializableClass as the correct identifier. But I would use the autogenerated identifier provided to you from Hibernate. You just need to make sure that the object has been inserted into the database before it is placed in another collection of objects.

+1
source

This is a method of equality, which should be reflective, transitive and symmetrical ...

The hashCode method must have these properties :

General hashCode contract:

Whenever it is called by the same object more than once during the execution of a Java application, the hashCode method must consistently return the same integer if the information used in equal comparisons with the object does not change. This integer should not remain consistent with one execution of the application on another execution of the same application.

If two objects are equal according to the equals (Object) method, then calling the hashCode method for each of the two objects should give the same integer result.

It is not required that if two objects are unequal according to the equals method (java.lang.Object), then calling the hashCode method for each of the two objects must produce different integer results. However, the programmer should be aware that obtaining separate integer results for unequal objects can improve the performance of hash tables.

Here, it seems that the hashCode used to put the record into the set during deserialization is different from the value calculated during contains (). By the way, since you noticed that the record is in the set, you simply cannot access it through hashCode, if you loop the contents of the set, you will find the elements.

Possible solutions:

  • hashCode does not rely on the parent object.
  • use a data structure that does not use hashcode (List, TreeSet ...)
  • do not use the contains method in Set ...
  • implement ReadResolve to recreate the Set after desirialization ...

[EDIT]: Looks like you're not alone bug_id = 4957674

0
source

Deserailization reads the values โ€‹โ€‹of both fields ( mHashCodeField and mSomeSet ) into a temporary array and after deserializing both values โ€‹โ€‹sets the fields to the stored values.

Since the HashSet recounts the hash codes of its elements during deserialization, it will use mHashCodeField when it is still zero.

A possible solution is to mark mSomeSet as transient and write / read in the writeObject / readObject file.

 @SuppressWarnings("unchecked") private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { System.out.println("Just started deserializing"); in.defaultReadObject(); mSomeSet=(Set<FieldOfSerializableClass>)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"); } 
0
source

Indeed, Hibernate says not to use id as hashcode, but I believe that they are too strict on this. This only makes sense if the identifier is auto-generated / auto-incremented by Hibernate. In this case, you may have a bean that gets its id value only when Hibernate decides to actually store it in the database, so in this situation you can get unpredictable behavior from the hashcode and / or equals method that uses the identifier. However, if the identifier is set manually, that is, your application is dealing with filling this value, then I believe that it is quite normal to use it in the hashcode / equals methods. This is for you?

0
source

I am adding another answer because it is very different from my first:

Here is an implementation that works without a transition field, I found the necessary information here: Advanced serialization here too .

By the way, I also tried using the serialPersistentFields attribute to force mHashCodeFields to serialize first, but that didn't help ...

  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<FieldOfSerializableClass> mSomeSet; public void setSomeSet(Set<FieldOfSerializableClass> pSomeSet) { mSomeSet = pSomeSet; } public Set<FieldOfSerializableClass> 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<FieldOfSerializableClass>)in.readObject(); System.out.println("In readObject - value of mHashCodeField: " + mHashCodeField); System.out.println("Just finished deserializing"); } } 
0
source

It seems to me that this is a bug in java, not the source code. Although the answers above provide good workarounds, the best solution would be Java to determine how deserialization works to account for circular references and sets / hash maps.

See here for a new bug report: http://bugreport.sun.com/bugreport/

The more people report this error, the more likely it is to fix it. I also get this error in my project, and the workarounds are much more than it costs me.

Also, here is a similar error report that I found: http://bugs.sun.com/view_bug.do;jsessionid=fb27da16bb769ffffffffebce29d31b2574e?bug_id=6208166

0
source

I ran into the same problem. I think you are right in your second reason rule. Here is my easiest problem replication:

 public class Test { static class Thing implements Serializable { String name; Set<Thing> others = new HashSet<Thing>(); @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(); } } 

Output:

 hashcode called with null name! 

This test fails. The easiest solution I found is to keep a copy of the hash code. Since it is primitive, it is set when the object is initialized during deserialization, and not later:

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

Now the test passes.

0
source

All Articles