post-images/overriding-equals/cover.jpg

Overriding equals()

Yesss 🎉 We continue with the first article of the new unit. In this article, I will cover the first item of Methods Common to All Objects, which is the second unit of the Effective Java book.

In this article, I will talk about how to override the equals() method in the Object class in our class, what we should pay attention to while overriding, and the critical errors that we should pay attention to while developing.

Let's visit our Object class, what's in this equals().

public boolean equals(Object obj) {  
    return (this == obj);  
}

It looks so simple and innocent, doesn't it? It's like an angel... It checks equality with the parameter given to the equals method for my class. However, it is very possible for me to be mistaken when I aim to customize this method. If I need to write a few main features of equals();

Each instance of my class is unique : Returns true for Threads that represent active entities instead of values. Exactly correct behavior for the Object class.
Two objects whose equality is checked do not have to be logically equal : For example, two generated Pattern objects do not need to match their patterns, or the Random() class does not need to produce the same number.
If the Parent class overrides the equals() method, this method also applies to the children: Set class gets equals() implementation from AbstractSet class, List class gets equals() implementation from AbstractList class.
It can be disabled with the private method : If you are obsessed with the possibility of using it and defining private is not enough for you, you can override it as follows.

@Override private boolean equals(Object o) {  
    throw new AssertionError(); // Method is never called  
}

I'm talking about the general properties of the equals() method. So when do I need to override this method? In which cases is this method overridden? Before answering these questions, I need to understand how this method, which is already present in the Object method, works. The (this == obj) check actually checks if these two objects are equal, and whether they represent the same place in memory, that is, whether their references are the same. For example, what would I do if I wanted to compare the values ​​that the objects contain logically, that is, not a reference, instead of a reference? Of course, I will override and rewrite this method myself. Here, I will refer to the first article of the book: Since an object created with a static factory method will constantly change its reference without changing its reference, the equals() method from the Object class will look at logical equality, not reference equality. This applies to 'Enum' types.

In order for the equals() implementation that you have made in your own object to work correctly and without error with the classes in the java library or other classes you have written, there is a set of rules, namely a contract. If we consider the articles of this contract with examples;

  • Reflexive : If x.equals(x) is true
  • Symmetric : If x.equals(y) = true then y.equals(x) is true.
  • Transitive : If x.equals(y) = true and y.equals(z) = true then x.equals(z) is true.
  • Consistent : Always x.equals(y) = true/false unless the implementation of the equals() method is changed (like true if it was true before).
  • Non-null : x.equals(null) is always false.

Note: Each parameter (x, y, z) mentioned in the examples are accepted as non-null.

As a critical issue, I would like to carefully mention that; Do not neglect even any article of the contract, you cannot be sure how the classes in java or added later will behave in front of your object. I think I will elaborate with examples so that you can better understand the situation here.

Reflexive

An object must be equal to itself. Otherwise, when I add from the object I wrote in a list and then call the contains() method of the list, it will not find my object even though I add it to the list and it will return false.

Symmetric

If my object is equal to another object, the object in question must also be equal to my object.

public final class CaseInsensitiveString {

    private final String s;
  
    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    // Broken - violates symmetry!
    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString)
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);

        if (o instanceof String) // One-way interoperability!
            return s.equalsIgnoreCase((String) o);

        return false;
    }
}

If I compared the code block I shared above with the following two objects in terms of equality, what do you think would be the result?

CaseInsensitiveString cis = new CaseInsensitiveString("Polish");  
String s = "polish";

If you noticed the equalsIgnoreCase() method used in the code block, you must have seen that the cis.equals(s) control will return true. Well, what happened to the rule "If x.equals(y) = true, y.equals(x) is true' brought by symmetry? There is an obvious violation of the rules in this currently written code block; Where if cis.equals(s) = true then s.equals(cis) = false. Because the String class is a class in java and the equals() method in it is case sensitive. Therefore, false will also be encountered in the following case.

List<CaseInsensitiveString> list = new ArrayList<>();  
list.add(cis);
...
list.contains(s); //false

As a solution, I can remove the check I made for the String class from within my code block.

@Override 
public boolean equals(Object o) {  
    return o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);  
}

Transitivity

This requirement is based on the logic that if the first object is equal to the second object and the second object is equal to the third object, the first object must be equal to the third object. Imagine a class with two attributes. I would like you to imagine that there is a child class that inherits this class and that there is an extra property in this child class. If the parent class I inherited has an overridden equals() method, it will also be valid in the child. When checking equality with a child with the same characteristics as the parent, it will break the transitivity principle as the parent will not have the characteristics of the child. I know it's hard to imagine like this, let's not stray from the way of sampling.

My parent class is;

class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}

My Child class is;

class ColorPoint extends Point {
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        super(x, y);
        this.color = color;
    }
}

The equality of the following two objects will return true. It works correctly according to the equals() method in Point, but it will return false for ColorPoint. Here we first broke the symmetric rule.

Point p = new Point(1, 2);  
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
....
p.equals(cp) // true
cp.equals(p) // false

To solve this situation, I can implement equals() on my ColorPoint.

// Broken - violates transitivity!
@Override 
public boolean equals(Object o) {  
    if (!(o instanceof Point))  
        return false;  

    // If o is a normal Point, do a color-blind comparison  
    if (!(o instanceof ColorPoint))  
        return o.equals(this);  

    // o is a ColorPoint; do a full comparison  
    return super.equals(o) && ((ColorPoint) o).color == color;  
}

Hmm... I think it seems like now p.equals(cp) and cp.equals(p) equality checks are turning true symmetric has taken care of 👌🏼 Only this time we broke transitivity 🤦🏻‍♂️

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);  
Point p2 = new Point(1, 2);  
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

When I examine it, the equality of p1.equals(p2) and p2.equals(p3) is true, but p1.equals(p3) equality is false. This shows that I am going against our transitivity clause.

So what's the solution? It is not possible to do this by inheritance. This is a fundamental problem of equality relation in object-oriented languages. The solution to this is the composition method. This subject will come up again in the next articles and I will examine it in detail. In summary, I have ensured that my parent and child are at the same level.

Solution with Composition Method:

// Adds a value component without violating the equals contract
class ColorPoint {
    private final Point point;
    private final Color color;

    public ColorPoint(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }

    // Returns the point-view of this color point.
    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

The Point object is not likely to be true because a property is missing. For ColorPoint objects, it will return false when color is not equal even if point is equal.

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);  
Point p2 = new Point(1, 2);  
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

Consistent

If two objects are equal, they should always remain equal unless one (or both) is changed. Isn't it very clear and clean, unlike Transitivity? I won't talk more about this 😅. The important issue here is that the values that we consider immutable are written accordingly in the equals() methods. While making a variable immutable, you should not ignore it if you are going to implement it in the equals() method where it is used.

Non-nullity

It is based on the principle that no object can be equal to null. I will not go into too much detail, if you have developed software before, you must have received the NullPointerException error. It is precisely this principle that lies at its foundation.

Before I end the topic, let me tell you a nice feature. While developing, we check for null values in many places, right?

// Explicit null check - unnecessary!
@Override 
public boolean equals(Object o) {  
    if (o == null)  return false;    
}

Here this check is unnecessary. Yes, it is unnecessary. If we were using the equals() method, we would use instanceof to determine the type of the object. While determining the instanceof object type, it also checks whether it is null.

// Implicit null check - preferred 
@Override 
public boolean equals(Object o) {  
    if (!(o instanceof MyType)) return false;  
}

Arrivederci 🙋🏻‍♂️

JavaEffective JavaMethods Common to All Objectsequals()OverridingSoftware Contract