Calling equals on an ArrayList After Serialization
I am hitting a strange problem in relation to equals on an object transported over RMI. This has been wrecking my head for a few days now and I was wondering if anyone can help shed some light on the Problem.
I have a Garage Class (that is also a JPA entity in case its relevant) that I push to a java process called X over RMI (So this object is being serialized). The Garage object stores a list of objects called Car (also JPA entities) that are also Serializable.
The equals method on Garage is basically calling equals on its list of cars (an ArrayList)
When I call equals in the java process it does not for some reason call equals on the list like i expect I would ex开发者_如何学Gopect it to call equals on all the Cars in the list to check if the lists are equal it does not do this.
The strange thing is when unit testing it does call equals on all the members of the Cars ArrayList. I even serialized the objects as part of my unit test and this worked too. Any ideas? I hope I am getting the problem across, feel free to request any info to clarify anything.
Edit: I am nearly certain its ArrayList being weird as when I manually do equals in my object instead of calling equals on the list of cars I did a foreach loop on the list of cars and called equals on each Car (like I expected ArrayList equals to do anyway and it worked as expected)
@Entity
@Table(schema="pdw", name="garage")
public class Garage
implements Comparable<Garage> ,
Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private String id;
private String name;
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(schema="pdw")
private List<Car> cars = new ArrayList<Car>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Car> getCars() {
return cars;
}
public void setCars(List<Car> cars) {
this.cars = cars;
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("[");
buffer.append("Garage:");
buffer.append("[id:" + id + "]");
buffer.append("[Cars:" + cars + "]");
buffer.append("]");
return buffer.toString();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Garage))
return false;
Garage other = (Garage) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (cars == null) {
if (other.cars != null)
return false;
} else if (!cars.equals(other.cars))
return false;
return true;
}
@Override
public int compareTo(Garage other) {
return this.getName().compareTo(other.getName());
}
}
@Entity
@Table(schema="pdw", name="car")
public class Car
implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private String id;
private String name;
@OneToOne(fetch = FetchType.LAZY)
private Garage garage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Garage getGarage() {
return garage;
}
public void setGarage(Garage garage) {
this.garage = garage;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Car other = (Car) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("[");
buffer.append("Car:");
buffer.append("[id:" + id + "]");
buffer.append("[name:" + name + "]");
buffer.append("[garage:" + garage.getName() + "]");
buffer.append("]");
return buffer.toString();
}
}
- Make sure your
List
isn't empty after deseriazliation. - Put a breakpoint in your
equals
method and see if anything wrong isn't happening - Make sure your implementation of
equals
onCar
is correct Check if there are no
transient
fieldsCheck if what you expect to be an
ArrayList
isn't actuallyPersistentBag
. Because itsequals
won't do what you want. If it isPersistentBag
, you can either transfer it to anArrayList
before sending over the wire (thus preventing a potentialLazyInitializationException
), or call equals on each element rather than on theList
itself. Hibernate usesPersistentBag
to wrap your collections in order to provide lazy loading
P.S. If you are usingi a JPA provider other than Hibernate, perhaps it has a similar collections wrappers. Indicate what's your persistence provider.
To expand Bozho:
- Check if the classes at the both sides have the same
serialVersionUID
(i.e. are of the exact same compiled versions). If necessary hardcode it asprivate static final long
class variable.
More about this in the java.io.Serializable
API and Sun's article about Serialization.
You mentioned that you are using JPA... make sure that the object contains a full ArrayList prior to serialization, perhaps you are lazy loading the list and it is empty after you serialize and deserialize the list? The only thing I don't understand (if this is the case) is why you are aren't getting errors for trying to lazy instantiate the list when not in session (as I suspect is the case on the deserialization side).
ArrayList
uses AbstractList
s implementation of equals()
. That is defined like this:
Compares the specified object with this list for equality. Returns true if and only if the specified object is also a list, both lists have the same size, and all corresponding pairs of elements in the two lists are equal. (Two elements e1 and e2 are equal if (e1==null ? e2==null : e1.equals(e2)).) In other words, two lists are defined to > be equal if they contain the same elements in the same order.
This implementation first checks if the specified object is this list. If so, it returns true; if not, it checks if the specified object is a list. If not, it returns false; if so, it iterates over both lists, comparing corresponding pairs of elements. If any comparison returns false, this method returns false. If either iterator runs out of elements before the other it returns false (as the lists are of unequal length); otherwise it returns true when the iterations complete.
If your Car
s are not being compared, maybe the comparison is already failing in the early parts of list comparison? Is it possible the lists you're comparing don't have the same number of elements?
If you have the Java sources installed you can debug through the AbstractList equals implementation and see where it is failing. Current implementation for Java 1.6 is:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator e2 = ((List) o).listIterator();
while(e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
return !(e1.hasNext() || e2.hasNext());
}
Apart from that a couple of comments, even though I don't think they are related to your issue:
1- If you override equals you have to override hashCode, I don't know if you have removed it on purpose or if you don't have it implemented. equals() and hashCode() are bound together by a joint contract that specifies if two objects are considered equal using the equals() method, then they must have identical hashcode values. (Borrowed from SCJP book). You will have issues with these classe in HashMaps, HashSets and other collection classes otherwise.
2- In your equals implementations, instanceof checks for both null-ness and the class type, you can replace
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
with
if (!(obj instanceof Car)){
return false;
}
It's possible you are getting subclasses for your objects (i know hibernate creates proxy classes for lazy loading support). in your Car class equals method, you do a "getClass()" comparison which will be false if you are comparing a proxy subclass to an actual instance. you could try the instanceof operation instead of getClass() (like in your Garage equals method). You could confirm all of this by including "getClass()" in your toString() method.
also, (again a lazy loading thing), you should never refer to member vars directly in an entity class, you should always use the getters and setters. (so your equals,toString,... methods should use getName(), etc.
Just a guess: isn't it possible that when you send the Garage
instance, the X process is only receiving a stub for it? If so, when you invoke the equals
method, it could be in fact executing a remote call to it, actually calling those methods in the original JVM (not in the X process).
You should be able to confirm that by adding breakpoints in both JVMs and invoking equals
.
I just ran into the same problem. JUnit's assertEquals() on collections returned by Hibernate will fail when Hibernate replaces your list with its PersistentBag class, because that class improperly implements equals(). Here's code from Hibernate 3.5.1-Final PersistentBag class:
/**
* Bag does not respect the collection API and do an
* JVM instance comparison to do the equals.
* The semantic is broken not to have to initialize a
* collection for a simple equals() operation.
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj) {
return super.equals(obj);
}
From reading their comment, it seems like they do this for performance/efficiency reasons. But it makes unit testing hard, if you have an object that contains a list. My solution will be writing an areEqualNonNullLists(listA, listB) method and put it in the eqauls() method of my object that contains a list.
public static boolean areEqualNonNullLists(List thisList, List thatList)
{
if(thisList.size() != thatList.size()) return false;
for(int i=0; i<thisList.size(); i++)
{
if(!thisList.get(i).equals( thatList.get(i) ) ) return false;
}
return true;
}
I wonder if there's a more elegant, generic solution.
精彩评论