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)
istrue
- Symmetric : If
x.equals(y) = true
theny.equals(x)
istrue
. - Transitive : If
x.equals(y) = true
andy.equals(z) = true
thenx.equals(z)
istrue
. - Consistent : Always
x.equals(y) = true/false
unless the implementation of theequals()
method is changed (liketrue
if it wastrue
before). - Non-null :
x.equals(null)
is alwaysfalse
.
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 🙋🏻♂️