Java .equals() instanceof subclass? Why not call superclass equals instead of making it final?

user2622016 picture user2622016 · Sep 2, 2013 · Viewed 11.3k times · Source

It is stated in Object's .equals(Object) javadoc:

It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.

Almost everywhere in example code I see overridden .equals(Object) method which uses instanceof as one of the first tests, for example here: What issues / pitfalls must be considered when overriding equals and hashCode?

public class Person {
    private String name;
    private int age;

    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (obj == this)
            return true;
        if (!(obj instanceof Person))
            return false;
        ...
    }

}

Now with class SpecialPerson extends Person having in equals:

        if (!(obj instanceof SpecialPerson))
            return false;

we con not guarantee that .equals() is symmetric. It has been discussed for example here: any-reason-to-prefer-getclass-over-instanceof-when-generating-equals

Person a = new Person(), b = new SpecialPerson();

a.equals(b);    //sometimes true, since b instanceof Person
b.equals(a);    //always false

Maybe I should add in the beginning of SpecialPerson's equals direct call to super?

    public boolean equals(Object obj) {
        if( !obj instanceof SpecialPerson )
            return super.equals(obj);
        ... 
        /* more equality tests here */
    }

Answer

Aaron Digulla picture Aaron Digulla · Sep 2, 2013

A lot of the examples use instanceof for two reasons: a) it folds the null check and type check into one or b) the example is for Hibernate or some other code-rewriting framework.

The "correct" (as per the JavaDoc) solution is to use this.getClass() == obj.getClass(). This works for Java because classes are singletons and the VM guarantees this. If you're paranoid, you can use this.getClass().equals(obj.getClass()) but the two are really equivalent.

This works most of the time. But sometimes, Java frameworks need to do "clever" things with the byte code. This usually means they create a subtype automatically. Since the subtype should be considered equal to the original type, equals() must be implemented in the "wrong" way but this doesn't matter since at runtime, the subtypes will all follow certain patterns. For example, they will do additional stuff before a setter is being called. This has no effect on the "equalness".

As you noticed, things start to get ugly when you have both cases: You really extend the base types and you mix that with automatic subtype generation. If you do that, you must make sure that you never use non-leaf types.