The equals() and hashCode() Contract
== versus .equals()
For reference types, == checks whether two variables point at the same object on the heap — identity comparison. It does not compare content.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false — different objects
System.out.println(a.equals(b)); // true — same content
Two distinct Integer objects with the same numeric value, or two distinct String objects with the same characters, can be ==-unequal but .equals()-equal.
For primitives (int, double, boolean, etc.), == is the correct comparison — there are no objects and no equals().
The confusion is amplified by the Integer cache. Integer.valueOf() caches values from to , so == “happens” to work for small numbers:
Integer x = 100; // autoboxed to Integer.valueOf(100), which is cached
Integer y = 100;
System.out.println(x == y); // true — same cached object
Integer p = 200; // outside the cache range
Integer q = 200;
System.out.println(p == q); // false — different objects
This is a trap. Always use .equals() for wrapper objects.
The equals() Contract
When overriding equals(), you must satisfy five properties (from the Object.equals() specification):
- Reflexive:
x.equals(x)must returntrue. - Symmetric:
x.equals(y)must return the same asy.equals(x). - Transitive: if
x.equals(y)andy.equals(z)aretrue, thenx.equals(z)must betrue. - Consistent: multiple invocations of
x.equals(y)must consistently return the same value, provided no information used in the comparison is modified. - Non-null:
x.equals(null)must returnfalse(not throwNullPointerException).
The hashCode() Contract
If you override equals(), you must also override hashCode():
- If
a.equals(b)istrue, thena.hashCode()must equalb.hashCode(). - The reverse is not required: two unequal objects may (by coincidence) have the same hash code — this is a collision and is expected.
If you break this contract, hash-based collections (HashMap, HashSet) silently malfunction:
class BrokenPoint {
int x, y;
BrokenPoint(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof BrokenPoint)) return false;
BrokenPoint p = (BrokenPoint) o;
return x == p.x && y == p.y;
}
// no hashCode() override — uses Object's identity-based hashCode
}
HashSet<BrokenPoint> set = new HashSet<>();
set.add(new BrokenPoint(1, 2));
System.out.println(set.contains(new BrokenPoint(1, 2))); // false!
The contains call fails. The lookup computes the (inherited identity-based) hash of the query object, checks that bucket, and finds nothing — the inserted object is in a different bucket because it had a different (identity-based) hash. The objects are .equals()-equal but have different hash codes, so the hash table cannot find one given the other.
A Proper Point Class
import java.util.Objects;
class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // fast path: same reference
if (!(o instanceof Point)) return false; // null or wrong type
Point other = (Point) o;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Objects.equals(a, b) handles null-safety for individual field comparisons. Objects.hash(...) generates a well-distributed hash from multiple fields — it is equivalent to Arrays.hashCode(new Object[]{x, y}).
instanceof versus getClass()
There are two schools of thought for the type check in equals():
instanceof (permits subclasses)
if (!(o instanceof Point)) return false;
A ColouredPoint subclass of Point can be .equals()-equal to a plain Point if their coordinates match. This is more flexible but can break symmetry — ColouredPoint.equals(Point) might return true while Point.equals(ColouredPoint) returns false unless both classes are carefully coordinated.
getClass() (forbids subclasses)
if (this.getClass() != o.getClass()) return false;
Only objects of the exact same class can be equal. This preserves symmetry automatically but prevents subclass instances from ever being equal to superclass instances.
The Tripos may expect you to identify this trade-off and justify your choice depending on the domain semantics. For most practical purposes, instanceof is more common, combined with making the class final if subclass equality is a concern.
Default Behaviour
Object.equals() is identity-based (equivalent to ==). Object.hashCode() typically returns a value derived from the object’s memory address (implementation-dependent). If you do not override them, every object is equal only to itself and has a unique hash — which is fine for classes where identity is the correct notion of equality (e.g., mutable stateful objects, threads, locks).
Override equals() and hashCode() when your class represents a value — something whose identity is determined by its data, not by which particular object in memory holds that data.