tree_title | description | last_modified |
---|---|---|
Equals |
How and why to write custom equals methods in Java |
2022-01-31 10:44:35 UTC |
- Why override standard equals?
- The requirements for a good
equals
method - Example class
- Naïve implementations
- Simple decent implementation
- Dealing with subclasses
- In practice
- Testing
equals
methods - Resources
By default, every Java object has an equals(Object o)
method which is inherited from the Object
class. The implementation of this equals
method compares objects using their memory locations, meaning that two objects are only considered equal if they actually point to the exact same memory location and are thus really one and the same object.
@Test
public void test() {
Object object1 = new Object();
Object sameObject = object1;
Object object2 = new Object();
assertTrue(object1.equals(sameObject)); // this succeeds
assertTrue(object1.equals(object2)); // this fails
}
If you want to define equality in such a way that two objects can be considered equal even if they are not really the exact same object in the exact same memory location, you will need a custom equals
implementation.
- Reflexivity: every object is equal to itself
- Symmetry: if a is equal to b, then b is also equal to a
- Transitivity: if a is equal to b and b is equal to c, then a is also equal to c
- Consistency: if a is equal to b right now, then a is always equal to b as long as none of their state that is used in the
equals
method has been modified - Non-nullity: an actual object is never equal to
null
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// getters and setters for x and y here
}
public boolean equals(Point other) {
return (this.x == other.x && this.y == other.y);
}
@Test
public void test() {
Point point1 = new Point(1, 1);
Point point2 = new Point(1, 1);
List<Point> points = Arrays.asList(point1);
assertTrue(point1.equals(point2)); // this succeeds
assertTrue(points.contains(point2)); // this fails
}
Problem: equals(Point)
does not properly override equals(Object)
because the signature doesn't match.
@Test
public void test() {
Point point1 = new Point(1, 1);
Object pointObject = new Point(1, 1);
assertTrue(point1.equals(pointObject)); // this fails
assertTrue(pointObject.equals(point1)); // also fails
}
- In the first assertion, we are calling a method with signature
equals(Object)
on an object with compile-time typePoint
. AsPoint
does not implement a method with that signature, the best match is theequals(Object)
method inherited fromObject
. - In the second assertion, we are calling a method with signature
equals(Point)
on an object with compile-time typeObject
. AsObject
does not have anequals(Point)
method, the best match at compile time is itsequals(Object)
method. And, becausePoint
(the run-time type ofpointObject
) does not override that method, the actual implementation that gets called is still the one defined inObject
.
See also Overloading, overriding and method hiding
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Point other = (Point) o;
return (this.x == other.x && this.y == other.y);
}
@Test
public void test() {
Point point1 = new Point(1, 1);
Point point2 = new Point(1, 1);
Set<Point> points = new HashSet<Point>();
points.add(point1);
assertTrue(points.contains(point2)); // this fails
}
Problem: HashSet
uses hashCode
, and default implementation is likely to return different hash codes for different objects (not same memory location)
// getters and setter for x and y here
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Point other = (Point) o;
return (this.x == other.x && this.y == other.y);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Test
public void test() {
Point point1 = new Point(1, 1);
Set<Point> points = new HashSet<Point>();
points.add(point1);
point1.setX(2);
assertTrue(points.contains(point1)); // this fails
}
Problem: changing x
also changes the hash code, which means that the hash bucket where the set now looks for the point is different from the hash bucket where the point ended up based on its initial hash code.
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// getters for x and y here
@Override
public boolean equals(Object o) {
if (o == this) {
return true; // optimization, this check is very fast
}
if (o == null || o.getClass() != this.getClass()) {
return false;
}
Point other = (Point) o;
return (this.x == other.x && this.y == other.y);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
}
The equals
and hashCode
methods are pretty much what Eclipse generates by default
Problem with equals
method using getClass
:
@Test
public void test() {
Point point1 = new Point(1, 1);
Point point2 = new Point(1, 1) {}; // anonymous subclass
assertTrue(point1.equals(point2)); // this fails
}
Reason: this.getClass()
returns different class for objects of different classes!
Solution: replace
o.getClass() != this.getClass()
with
!(o instanceof Point)
Most IDEs have option to do this when generating equals
.
What if some subclasses of Point
have additional info to consider when determining if objects are equal?
Example:
public enum Color {
BLUE, RED, YELLOW, GREEN;
}
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
// getter for color
}
What if we want to include the color in the equals
method so that a ColorPoint(1, 1, Color.RED)
is not equal to a ColorPoint(1, 1, Color.BLUE)
?
If we want this, we have to accept that a ColorPoint
will never be equal to any Point
. The reason for this is transitivity (see above). If we want to say that ColorPoint(1, 1, Color.RED)
and ColorPoint(1, 1, Color.BLUE)
are both equal to Point(1, 1)
, then transitivity would imply that they are also equal to each other. That is exactly what we didn't want here.
This could be seen as a violation of the Liskov substitution principle
@Test
public void test() {
Point point1 = new Point(1, 1);
Point point2 = new ColorPoint(1, 1, Color.BLUE);
assertTrue(point1.getX() == point2.getX());
assertTrue(point1.getY() == point2.getY());
assertTrue(point1.equals(point2)); // this fails
}
public class Point {
// ...
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point other = (Point) o;
if (!other.canEqual(this)) {
return false;
}
return (this.x == other.x && this.y == other.y);
}
public boolean canEqual(Object o) {
return (o instanceof Point);
}
// ...
}
public class ColorPoint extends Point {
// ...
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) {
return false;
}
ColorPoint other = (ColorPoint) o;
if (!other.canEqual(this)) {
return false;
}
return (this.color == other.color
&& super.equals(other));
}
public boolean canEqual(Object o) {
return (o instanceof ColorPoint);
}
// ...
}
Benefits:
- passes all of the previous tests
- still allows subclasses of
Point
that do not include additional state to be equal to aPoint
Simply use getClass()
again!
Drawback: objects can only be equal if they are or exactly the same class
Recommended approach:
- Let your IDE generate your
equals
(andhashCode
) methods for you, usinginstanceof
instead ofgetClass()
. - Either make your class
final
or make yourequals
andhashCode
methodsfinal
.
Note that the two options outlined in step 2 have different effects:
- Making your class
final
prevents any issues with subclasses by simply not allowing subclasses for your class. - Making your
equals
andhashCode
methodsfinal
prevents subclasses from overriding yourequals
andhashCode
methods and including additional state in them.
In cases where this is not sufficient (you want subclasses to include additional state in their equals
method), consider using the solution involving the canEqual
method or the simpler solution using getClass
if you’re ok with subclass instances never being equal to superclass instances.
Better alternative to hand-written equals
tests: the EqualsVerifier library by Jan Ouwens.
@Test
public void equalsContract() {
EqualsVerifier.forClass(Point.class).verify();
}
Uses reflection to inspect class and test its equals
and hashCode
methods with 100% coverage.
Overview of detected errors. It is also possible to suppress certain errors.
- EqualsVerifier
- How to Write an Equality Method in Java
- Core Java SE 9 for the Impatient (book by Cay S. Horstmann)
- Overloading in the Java Language Specification