13

Reading: Effective Java - Second Edition by Joshua Bloch

Item 8 - Obey the general contract when overriding equals states:

It is not uncommon for a programmer to write an equals method that looks like this, and then spend hours puzzling over why it doesn't work properly:

[Code sample here]

The problem is that this method does not override Object.equals, whose argument is of type Object, but overloads it instead.

Code Sample:

public boolean equals(MyClass o) {
    //...
}

My Question:

Why is a strongly typed equals method that overloads like the one in this code sample not sufficient? The book states that overloading rather than overriding is bad, but it doesn't state why this is the case or what scenarios would make this equals method fail.

John Humphreys
  • 37,047
  • 37
  • 155
  • 255

3 Answers3

20

This is because overloading the method won't change the behavior in places like collections or other places that the equals(Object) method is explicitly used. For example, take the following code:

public class MyClass {

    public boolean equals(MyClass m) {
        return true;
    }
}

If you put this in something like a HashSet:

public static void main(String[] args) {
    Set<MyClass> myClasses = new HashSet<>();
    myClasses.add(new MyClass());
    myClasses.add(new MyClass());
    System.out.println(myClasses.size());
}

This will print 2, not 1, even though you'd expect all MyClass instances to be equal from your overload and the set wouldn't add the second instance.

So basically, even though this is true:

MyClass myClass = new MyClass();
new MyClass().equals(myClass);

This is false:

Object o = new MyClass();
new MyClass().equals(o);

And the latter is the version that collections and other classes use to determine equality. In fact, the only place this will return true is where the parameter is explicitly an instance of MyClass or one of its subtypes.


Edit: per your question:

Overriding versus Overloading

Let's start with the difference between overriding and overloading. With overriding, you actually redefine the method. You remove its original implementation and actually replace it with your own. So when you do:

@Override
public boolean equals(Object o) { ... }

You're actually re-linking your new equals implementation to replace the one from Object (or whatever superclass that last defined it).

On the other hand, when you do:

public boolean equals(MyClass m) { ... }

You're defining an entirely new method because you're defining a method with the same name, but different parameters. When HashSet calls equals, it calls it on a variable of the type Object:

Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

(That code is from the source code of HashMap.put, which is used as the underlying implementation for HashSet.add.)

To be clear, the only time it will use a different equals is when an equals method is overridden, not overloaded. If you try to add @Override to your overloaded equals method, it will fail with a compiler error, complaining that it doesn't override a method. I can even declare both equals methods in the same class, because it's overloading:

public class MyClass {

    @Override
    public boolean equals(Object o) {
        return false;
    }

    public boolean equals(MyClass m) {
        return true;
    }
}

Generics

As for generics, equals is not generic. It explicitly takes Object as its type, so that point is moot. Now, let's say you tried to do this:

public class MyGenericClass<T> {

    public boolean equals(T t) {
        return false;
    }
}

This won't compile with the message:

Name clash: The method equals(T) of type MyGenericClass has the same erasure as equals(Object) of type Object but does not override it

And if you try to @Override it:

public class MyGenericClass<T> {

    @Override
    public boolean equals(T t) {
        return false;
    }
}

You'll get this instead:

The method equals(T) of type MyGenericClass must override or implement a supertype method

So you can't win. What's happening here is that Java implements generics using erasure. When Java finishes checking all the generic types on compile time, the actual runtime objects all get replaced with Object. Everywhere you see T, the actual bytecode contains Object instead. This is why reflection doesn't work well with generic classes and why you can't do things like list instanceof List<String>.

This also makes it so that you can't overload with generic types. If you have this class:

public class Example<T> {
    public void add(Object o) { ... }
    public void add(T t) { ... }
}

You'll get compiler errors from the add(T) method because when the classes are actually done compiling, the methods would both have the same signature, public void add(Object).

Brian
  • 17,079
  • 6
  • 43
  • 66
  • +1, good example :) why do containers like HashSet prefer Object.Equals over calling the .Equals method of their template-parameter class (MyClass in your example)? – John Humphreys Oct 08 '12 at 19:23
  • It's also worth noting that Java doesn't have any kind of dynamic method resolution for overloaded methods. I'm not sure if there are any strongly-typed languages (Java, C#, C++, etc.) that do. The JVM won't check to see if it's passing a `MyClass` and call the overloaded method instead. It just sees that it's supposed to call `equals(Object)` on `Object`, and when it does, it checks for methods that _override_ `equals(Object)`. – Brian Oct 08 '12 at 19:51
  • I'd assume that Java's HashSet actually calls Object.hashCode() or its overrides to determine equality of the objects inserted, like C# does. – Tom Lint Oct 18 '13 at 14:45
  • @TomLint No. C# and Java *both* use the `hashCode` to determine the bucket of the linked list, but they **DO NOT** use the `hashCode` to determine equality. [Relevant link](http://social.msdn.microsoft.com/Forums/vstudio/en-US/dc415bcb-68fe-4619-9c25-acd8924f731f/hash-table-buckets?forum=csharpgeneral). – Brian Oct 18 '13 at 21:28
12

Why is a strongly typed equals method that overloads like the one in this code sample not sufficient?

Because it doesn't override Object.equals. Any general purpose code which only knows about the method declared in Object (e.g. HashMap, testing for key equality) won't end up calling your overload - they'll just end up calling the original implementation which gives reference equality.

Remember that overloading is determined at compile-time, whereas overriding is determined at execution time.

If you're overriding equals, it's often a good idea to provide a strongly-typed version as well, and delegate to that from the method declared in equals.

Here's a complete example of how it can go wrong:

import java.util.*;

final class BadKey {    
    private final String name;

    public BadKey(String name) {
        // TODO: Non-nullity validation
        this.name = name;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    public boolean equals(BadKey other) {
        return other != null && other.name.equals(name);
    }
}

public class Test {

    public static void main(String[] args) throws Exception {
        BadKey key1 = new BadKey("foo");
        BadKey key2 = new BadKey("foo");
        System.out.println(key1.equals(key2)); // true

        Map<BadKey, String> map = new HashMap<BadKey, String>();
        map.put(key1, "bar");
        System.out.println(map.get(key2)); // null
    }
}

The fix is simply to add an override, like this:

@Override
public boolean equals(Object other) {
    // Delegate to the more strongly-typed implementation
    // where appropriate.
    return other instanceof BadKey && equals((BadKey) other);
}
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    Thanks :) so, why wouldn't the HashMap, for example, invoke the class-specific equality method? Wouldn't the template/generics mechanism in Java let the containers invoke the method on the template parameter type rather than simply calling Object.equals? – John Humphreys Oct 08 '12 at 19:21
  • 3
    `equals()` is not generic. It takes an Object as argument, not a generic type. And collections can hold any kinds of objects, so they must be able to compare apples with oranges, and not just apples with apples. – JB Nizet Oct 08 '12 at 19:30
  • Ah, so if equals() was generic, would it work? Or would type erasure cause it to still invoke the Object.equals() method? Or could you even create a generic equals...? – xdhmoore Jun 27 '13 at 23:21
  • @xdhmoore You could have a generic `equals()`, this would work similar to the `compareTo(T o)` method used in ordered collections. Imagine an `Equicomparable` interface with one method, `public boolean equals(T o)`. But you'd have to be careful, because using the wrong type of object would result in a call to `Object.equals()`, instead of a type error. – augurar Jul 21 '14 at 18:25
2

Because the collections using equals will use the Object.equals(Object) method (potentially overridden in MyClass, and thus called polymorphically), which is different from the MyClass.equals(MyClass).

Overloading a method defines a new, different method, that happens to have the same name as another one.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255