views:

2634

answers:

7

I ran into an interesting behavior recently. It seems that if I override .equals() to take a parameter other than Object, it doesn't get called. Can anyone explain to me why this is happening? It seems to violate my understanding of polymorphism in OOP, but maybe I'm missing something.

Here's much simpler code that shows what I'm seeing:

public class MyClass {
  private int x;
  public MyClass(int n) { x = n; }
  public boolean equals(Object o) { return false; }
  public boolean equals(MyClass mc) { return x == mc.x; }
  public static void main(String[] args) {
    List<MyClass> list = new ArrayList<MyClass>();
    list.add(new MyClass(3));
    System.out.println("Contains 3? " + list.contains(new MyClass(3)));
  }
}

When this is run, it prints "Contains 3? false". It looks like the equals(Object) function is called, even though there is another that would work. By contrast, if I write equals like this the code works as expected:

public boolean equals(Object o) {
  if(!(o instanceof MyClass))
    return false;
  MyClass mc = (MyClass)o;
  return x == mc.x;
}

Why isn't it figuring out which version of the function to call based on the type of the parameter?

+8  A: 

equals(Object) is overriding a super method; you can not override a super method without using the exact same signature (Well, there are some exceptions like covariant returntypes and exception).

p3t0r
A: 

Ok let me re-phrase.

(1)Because the compiler eliminates all information regarding to Generics (erasure, see here), and (2) because you cannot override a method without the exact same signature (equals(Object)), (3) during runtime all objects inside the List are treated as Objects and not as instances of MyClass. Hence, the method that gets called is equals(Object) since this is the one that is been overwritten by your class.

Elliot Vargas
The problem has nothing to do with erasure.
Darron
@Darron: Not quite true. If the main method had been "System.out.println(new MyClass(1).equals(new MyClass(1)));" it would have printed "true".
Michael Myers
... but because of erasure, it doesn't know about the equals(MyClass) method (which doesn't override the one it's calling).
Michael Myers
The concrete problem is that List.contains receives an Object and hence the overwritten Object.equals method gets called.I don't have a problem with explaining override vs overload, but that is not solving the concrete problem.My answer may not be complete but I don't think it deserves down votes.
Elliot Vargas
+13  A: 

You're mixing up "overriding" and "overloading".

Overriding -- adding a replacement definition of an existing method for purposes of polymorphism. The method must have the same signature. The signature consists of the name and argument types. Overridden methods are selected at runtime based on the runtime type of the target object.

Overloading -- adding a method with the same name but a different signature. Overloaded methods are selected at compile time based on the compile time type of the target object.

Darron
+3  A: 

there are different types of http://en.wikipedia.org/wiki/Polymorphism_(computer_science). java does not do http://en.wikipedia.org/wiki/Double_dispatch.

Ray Tayek
+1  A: 

The ArrayList implementation of the contains(Object) method is bound to use Object.equals(Object) method internally, so it'll never know about your overloading of the equals(MyClass) method. Only an overriding method (with matching signature) will be found.

John Flinchbaugh
A: 

You're assuming that the contains() method in List knows the type of the object at runtime, which is incorrect.

Because of erasure, List<MyClass> becomes just a regular List at runtime, so the contains() method sees its parameter as an Object, thus invoking Object's equals() instead of the one you defined for MyClass in its execution.

InverseFalcon
+1  A: 

Notice that the method you are calling is defined in the javadoc for ArrayList<E> as

boolean contains(Object o)
    Returns true if this list contains the specified element.

instead of

boolean contains(E o)
    Returns true if this list contains the specified element.

Implementation of ArrayList.java:

private transient Object elementData[];

public boolean contains(Object elem) {
    return indexOf(elem) >= 0;
}

public int indexOf(Object elem) {
    if (elem == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (elem.equals(elementData[i]))
                return i;
    }
    return -1;
}

It uses the equals method defined in the Object superclass since the equals method is not overridden in ArrayList<E>'s implementation.

When overriding Object equals in java, you should override the Object hashCode method as well.

Anyway you might want to try the following code:

class A{    
    public int content;    
    A(){
        this(0);
    }    
    A(int value){
        content = value;
    }   
    public boolean equals(Object obj){
        System.out.println("overriding equals method");
        return this.content == ((A) obj).content;
    }    
    public boolean equals(A a){
        System.out.println("overloading equals method");
        return this.content == a.content;
    }    
    public static void main(String[] args){
        A x = new A(1);
        A y = new A(2);
        Object z  = new A(1);
        System.out.println(x.equals(y));
        System.out.println(x.equals(x));
        System.out.println(x.equals(z));
        //override as z is declared as Object at compile time
        //so it will use methods in class Object instead of class A
        System.out.println(x.equals((Object) y));
        System.out.println(x.equals((Object) x));        
    }   
}
//rant: they didn't teach me these in javaschool and I had to learn it the hard way.
blizpasta