Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding Object.equals VS Overloading it

Tags:

java

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.

like image 946
John Humphreys Avatar asked Oct 08 '12 19:10

John Humphreys


People also ask

Why does it generally make more sense to override the equals method than to overload it?

Why does it generally make more sense to override the equals method than to overload it? If equals is overridden, then you end up with two equals methods for the same class.

What is the reason for overriding equals () method?

We can override the equals method in our class to check whether two objects have same data or not.

Can we override equals method in Java?

You can override the equals method on a record, if you want a behavior other than the default. But if you do override equals , be sure to override hashCode for consistent logic, as you would for a conventional Java class.

Can == be overridden?

Terminology note: == is never overridden. It's overloaded.


1 Answers

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

like image 131
Brian Avatar answered Sep 28 '22 03:09

Brian