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; } } }