I have some doubts about equals
and hashCode
contract in Java using EqualsVerifier library.
Imagine we have something like this
public abstract class Person {
protected String name;
@Override
public boolean equals(Object obj) {
// only name is taken into account
}
@Override
public int hashCode() {
// only name is taken into account
}
}
And the following extended class:
public final class Worker extends Person {
private String workDescription;
@Override
public final boolean equals(Object obj) {
// name and workDescription are taken into account
}
@Override
public final int hashCode() {
// name and workDescription are taken into account
}
}
I try to test whether I fulfill the equals
and hashCode
contract in the Person class, using EqualsVerifier
@Test
public void testEqualsAndHashCodeContract() {
EqualsVerifier.forClass(Person.class).verify();
}
Running this test, I get that I have to declare equals
and hashCode
methods final, but this is something that I don't want to do, because I may want to declare these two methods in the extended classes, since I want to use some child's attributes in equals
and hashCode
.
Could you skip for testing the final rule in the EqualsVerifier library? Or am I missing something?
Disclaimer: I'm the creator of EqualsVerifier. I only just discovered this question :).
The workaround Joachim Sauer mentions is correct.
Let me explain why EqualsVerifier does not like your implementation. Let's pretend for now that Person
is not abstract; it makes the examples a bit simpler. Let's say we have two Person
objects, like this:
Person person1 = new Person("John");
Person person2 = new Worker("John", "CEO of the world");
And let's call equals
on both these objects:
boolean b1 = person1.equals(person2); // returns true
boolean b2 = person2.equals(person1); // returns false
b1
is true, because Person
's equals
method is called, and it ignores workDescription
. b2
is false, because Worker
's equals
method is called, and the instanceof
or getClass()
check in that method returns false.
In other words, your equals
method is no longer symmetric, and this is a requirement for a correct implementation of equals
, according to the Javadoc.
You can indeed use getClass()
to get around this problem, but then you run into another problem. Let's say you use Hibernate, or a mocking framework. These frameworks use bytecode manipulation to create subclasses of your class. Essentially, you'll get a class like this:
class Person$Proxy extends Person { }
So let's say you make a round trip to the database, like this:
Person person1 = new Person("John");
em.persist(person1);
// ...
Person fetchedPerson = em.find(Person.class, "John");
And now let's call equals
:
boolean b3 = person1.equals(fetchedPerson); // returns false
boolean b4 = fetchedPerson.equals(person1); // also returns false
b3
and b4
are false because person1
and fetchedPerson
are of different classes (Person
and Person$Proxy
, to be precise). equals
is symmetric now, so at least it follows the contract, but it's still not what you want: fetchedPerson
doesn't "behave" like a Person
anymore. In technical terms: this breaks the Liskov Substitution Principle, which is the basis for Object-Oriented Programming.
There is a way to make all this work, but it's quite complicated. (If you really want to know: this article explains how.) To keep things simple, EqualsVerifier suggests that you make your equals
and hashCode
methods final. In most cases, this will work fine. If you really need to, you can always take the complicated route.
In your case, since Person
is abstract, you could also choose to not implement equals
in Person
, but only in Worker
(and any other subclasses you may have).
Getting that right is very tricky.
The documentation of EqualsVerifier explains a workaround:
EqualsVerifier.forClass(MyClass.class)
.withRedefinedSubclass(SomeSubclass.class)
.verify();
Note that for this to work, you probably need to check getClass()
in your equals because a Worker
can (or should) never be equal to a Person
.
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