Question

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?

Was it helpful?

Solution

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

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top