Equality and hash codes
Java has two kinds of equality. == checks that two variables point to the same object in memory. The equals method checks that objects are equal in meaning — for example, two strings consist of the same characters. Collections like HashSet and HashMap also rely on the hash code — a number an object returns from its hashCode() method.
== and equals
Let's create two identical strings. These are two different objects with the same contents:
void main() {
String a = new String("cat");
String b = new String("cat");
IO.println(a == b);
IO.println(a.equals(b));
}Output:
false
true== returned false: the objects are different. equals returned true: the contents match. That's why strings and other objects are always compared with equals, and == is left for primitives such as int.
The default equals
If a class doesn't override equals, it inherits the version from Object, which works like ==. Two "identical" objects are considered different:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
void main() {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
IO.println(p1.equals(p2));
}Output:
falseYour own equals and hashCode
To compare objects by their fields, you override equals. Along with it you must override hashCode. The rule (the contract) is: if two objects are equal by equals, their hashCode values must match. Handy helpers are Objects.equals (compares safely even with null) and Objects.hash:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
void main() {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
IO.println(p1.equals(p2));
IO.println(p1.hashCode() == p2.hashCode());
}Output:
true
trueequals takes an Object, so we check the type first — the instanceof pattern comes in handy here.
Why hashCode is needed
HashSet and HashMap sort objects into "buckets" by their hash code. To find an object, the collection first computes the hash code, goes to the right bucket and only there compares with equals. That's why equal objects must have equal hash codes — otherwise the collection looks in the wrong bucket. Different objects, on the other hand, may happen to share a hash code — that's fine:
void main() {
IO.println("Aa".hashCode());
IO.println("BB".hashCode());
IO.println("Aa".equals("BB"));
}Output:
2112
2112
falseRecords do it themselves
Records get equals, hashCode and toString generated automatically from all their components. That makes records the best choice for objects compared by value:
record Point(int x, int y) {}
void main() {
Set<Point> visited = new HashSet<>();
visited.add(new Point(1, 2));
visited.add(new Point(1, 2));
visited.add(new Point(3, 4));
IO.println(visited.size());
IO.println(visited.contains(new Point(3, 4)));
}Output:
2
trueCommon mistake
If you override only equals and forget hashCode, the objects are equal but have different hash codes. A HashSet puts them into different buckets and treats them as different:
class Point {
final int x;
final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
return o instanceof Point other && x == other.x && y == other.y;
}
}
void main() {
Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.add(new Point(1, 2));
IO.println(new Point(1, 2).equals(new Point(1, 2)));
IO.println(points.size());
IO.println(points.contains(new Point(1, 2)));
}Output:
true
2
falseThere's no compile error, but the collection misbehaves. Always override equals and hashCode together — or use records, which do it for you.