Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding equals method

Tags:

java

equals

J. Bloch in his effective Java provides a several rules for the implementation for equals method. Here they are:

• Reflexive: For any non-null reference value x, x.equals(x) must return true.

• Symmetric: For any non-null reference values x and y, x.equals(y) must return true if and only if y.equals(x) returns true.

• Transitive: For any non-null reference values x, y, z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) must return true.

• Consistent: For any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.

• For any non-null reference value x, x.equals(null) must return false.

But later in the book he mentioned so-called Liskov substitution principle:

The Liskov substitution principle says that any important property of a type should also hold for its subtypes, so that any method written for the type should work equally well on its subtypes

I don't see how it ties to the equals contracts. Should we actually adhere to it while writing the equals implementation?

The question is about implementing the method for subclasses. Here is the example from the book:

private static final Set<Point> unitCircle;

static {
    unitCircle = new HashSet<Point>();
    unitCircle.add(new Point(1, 0));
    unitCircle.add(new Point(0, 1));
    unitCircle.add(new Point(-1, 0));
    unitCircle.add(new Point(0, -1));
}

public static boolean onUnitCircle(Point p) {
    return unitCircle.contains(p);
}

public class CounterPoint extends Point {
    private static final AtomicInteger counter = new AtomicInteger();

    public CounterPoint(int x, int y) {
        super(x, y);
        counter.incrementAndGet();
    }

    public int numberCreated() { return counter.get(); }
}

and the following implementation:

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

Ok, violates and what then? I don't understand.

like image 617
St.Antario Avatar asked Jun 23 '15 09:06

St.Antario


2 Answers

There are typically 2 ways how to check the type in the equals method:

Option 1: instanceof

if (! (obj instanceof ThisClass)){
    return false;
}

This option respects the Liskov substitution principle. But you cannot add additional properties in sub classes which are relevant for the equals method without breaking the characteristics of an equivalence relation (reflexive, symmetric, transitive).

Option 2: getClass()

if (obj == null || ! this.getClass().equals(obj.getClass())) {
    return false;
}

This option violates the Liskov substitution principle. But you can add additional properties in sub classes which are relevant for the equals method without breaking the characteristics of an equivalence relation (reflexive, symmetric, transitive).

Joshua Bloch warns about this in his book "Effective Java".

Angelika Langer however mentions a way for "mixed-tpye" comparisons, if you can define default values for additional properties:

http://www.angelikalanger.com/Articles/JavaSolutions/SecretsOfEquals/Equals-2.html

The downside is that the equals methods becomes rather complicated.

// Broken - violates Liskov substitution principle (page 40)
@Override public boolean equals(Object o) {
    if (o == null || o.getClass() != getClass())
        return false;
    Point p = (Point) o;
    return p.x == x && p.y == y;
}

Ok, violates and what then? I don't understand.

So if you have a sub class such as MyPoint (which might add additional methods but not additional properties/ fields), then

Point p1 = new Point(x, y);
Point p2 = new MyPoint(x, y);

p1.equals(p2) == false

Set<Point> points = new HashSet<>();
points.add(p1);

points.contains(p2) == false;

although both objects really represent the same point.

If you would use option 1 (instanceof) instead, the equals method would return true.

like image 72
Puce Avatar answered Sep 28 '22 05:09

Puce


I think he is trying to say that the characteristic of a point is its coordinates. So you would expect this to be true:

new Point(0, 0).equals(new CounterPoint(0, 0));

because the two points have the same coordinates, even if they don't have the same type. But the proposed equals method will return false because the two objects have different classes.

If you think of collections for example, this is true:

new LinkedList().equals(new ArrayList());

The two lists don't have the same type but they have the same content (in this case they are both empty) and are therefore considered equal.

like image 30
assylias Avatar answered Sep 28 '22 05:09

assylias