Overriding Object.equals VS Overloading it

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

Leave a Comment