views:

348

answers:

7

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 expect 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();
    } 
}
+2  A: 
  • 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 on Car is correct
  • Check if there are no transient fields

  • Check if what you expect to be an ArrayList isn't actually PersistentBag. Because its equals won't do what you want. If it is PersistentBag, you can either transfer it to an ArrayList before sending over the wire (thus preventing a potential LazyInitializationException), or call equals on each element rather than on the List itself. Hibernate uses PersistentBag 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.

Bozho
Thanks I can confirm the above are ok.
Paul Whelan
+1  A: 

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 as private static final long class variable.

More about this in the java.io.Serializable API and Sun's article about Serialization.

BalusC
yes I can confirm the serialVersionUID is ok thanks for the suggestion
Paul Whelan
With other words, you have hardcoded it? Are you sure that the classpath is clean, i.e. there are no older versioned classes elsewhere in the classpath?
BalusC
Yes I have a clean classpath and I have a serialVersionUID of 1
Paul Whelan
+1  A: 

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).

PaulP1975
Thanks Paul I can confirm the object has all the cars before the push over rmi.
Paul Whelan
+1  A: 

ArrayList uses AbstractLists 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 Cars 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?

Carl Smotricz
Thanks Carl thats what i first supsected but when I do the compare via a foreach for all the elements it works I actually added a println in the loop and each car was present and when I overrode the equals on the list it worked as I expected the ArrayList to do. I will recheck this again however thank you.
Paul Whelan
A: 

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.

Chuim
+1  A: 

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;
}
Iker Jimenez
+1  A: 

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.

james
Thanks James I'll try this out.
Paul Whelan