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.
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.
We can override the equals method in our class to check whether two objects have same data or not.
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.
Terminology note: == is never overridden. It's overloaded.
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)
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With