Java equals and hashCode Contract and Object Class Methods
Override equals and hashCode together in Java: equal objects need equal hash codes. The equals contract, a correct implementation, HashMap, toString and clone.
- Course: Java study plan
- Module: OOP II — inheritance & polymorphism
- Kind: Lesson
- Reading time: 16 min
- Runtime: OpenJDK 18
What is the equals and hashCode contract in Java?
The equals and hashCode contract in Java says that objects equal by equals must return the same hashCode; unequal objects may share one. equals itself must be reflexive, symmetric, transitive, consistent, and return false for null. HashMap and HashSet find entries by hash code first and equals second, so breaking the contract makes objects vanish from them.
Lesson
Every class inherits eleven methods from java.lang.Object. Three of them — equals, hashCode and toString — define how your objects compare, hash and print, and every collection, every map and every log line depends on them. Their default implementations are identity-based; the moment a class is a value, you override them together, according to a contract the whole library assumes. This lesson is that contract and the correct way to meet it.
What Object provides
| Method | Default behaviour |
|---|---|
equals(Object) | this == o — identity |
hashCode() | A number derived from the object's identity (not the address, but stable for its life) |
toString() | getClass().getName() + "@" + Integer.toHexString(hashCode()) |
getClass() | The runtime Class object — final, cannot be overridden |
clone() | Shallow field copy, protected, needs Cloneable — legacy, avoid |
finalize() | Deprecated for removal |
wait, notify, notifyAll | Thread coordination — Module 17, final |
The equals contract
For any non-null references x, y, z:
- Reflexive:
x.equals(x)is true. - Symmetric:
x.equals(y)⇔y.equals(x). - Transitive:
x.equals(y)andy.equals(z)⇒x.equals(z). - Consistent: repeated calls give the same result while the objects are unchanged.
x.equals(null)is false — never throws.
And the bridge to hashing: equal objects must have equal hash codes. (Unequal objects may share a hash code; that is a collision, and allowed.)
The canonical equals
@Override
public boolean equals(Object o) {
if (this == o) return true; // fast path, and reflexivity
if (o == null || getClass() != o.getClass()) return false; // type check
Point other = (Point) o;
return x == other.x && y == other.y; // field comparison
}
Line by line:
- The parameter is
Object.equals(Point p)is an overload thatHashSet,List.containsandObjects.equalsnever call. getClass() != o.getClass()makesequalsfalse across different classes, including subclasses — which keeps symmetry when a subclass adds fields. The alternativeo instanceof Pointlets subclasses be equal to parents and needs care (aColorPointequal to aPointbut not vice versa violates symmetry). Useinstanceofonly when subclasses add no state or when the class isfinal.- Compare primitives with
==,double/floatwithDouble.compare(a, b) == 0(handles NaN and −0.0), references withObjects.equals(a, b)(null-safe), arrays withArrays.equals.
The matching hashCode
@Override
public int hashCode() {
return Objects.hash(x, y); // combines the same fields equals uses
}
Objects.hash(...) boxes and builds an array — fine for most classes. For hot paths, the hand-rolled form:
int h = Integer.hashCode(x);
h = 31 * h + Integer.hashCode(y);
h = 31 * h + (name == null ? 0 : name.hashCode());
return h;
Rules: use exactly the fields equals uses (a subset is legal; a superset breaks the contract); never use a field that is mutable if the object will be a map key; a constant hashCode is legal and turns every HashMap into a linked list.
Why the contract matters: HashMap
map.put(key, v) computes key.hashCode(), picks a bucket, and stores the entry. map.get(k2) computes k2.hashCode(), goes to that bucket, and compares with equals. If k2.equals(key) but their hash codes differ, the lookup searches the wrong bucket and finds nothing. Override equals without hashCode and your objects vanish from hash-based collections — a bug that appears as "contains returned false for something I just added". HashSet, HashMap, Hashtable, ConcurrentHashMap and LinkedHashMap all depend on it.
Mutating a key after insertion is the other classic: the entry sits in the bucket computed from the old hash. Keys should be immutable.
toString
Covered in Module 7: override it early, include identifying fields, keep it single-line, never rely on parsing it. Records generate Point[x=1, y=2]; IDEs generate the same shape. Objects.toString(o, "default") is the null-safe helper.
Records do all three
record Point(int x, int y) { }
A record (Module 10) generates equals, hashCode and toString over its components, correctly, with Double.compare semantics for doubles and Arrays-unaware equality for array components (a reason not to put arrays in records). For value classes, records are the modern answer; the hand-written form above is for classes that cannot be records.
getClass and class literals
obj.getClass() returns the runtime Class<?>; Point.class is the compile-time literal for the same object. getClass().getSimpleName() is handy in toString. getClass() == Point.class is an exact-class test; instanceof includes subclasses.
clone — why you avoid it
Object.clone() is protected, performs a shallow copy, and throws CloneNotSupportedException unless the class implements the marker interface Cloneable — an API that fights the language (no constructor runs, final fields cannot be deep-copied). Arrays are the one place clone() is idiomatic. For everything else, a copy constructor or a static copyOf factory.
Common mistakes
| Mistake | Consequence |
|---|---|
equals(Point) instead of equals(Object) | Overload; collections use identity |
equals without hashCode | Lost in hash-based collections |
hashCode using fields equals ignores | Equal objects, different hashes |
Comparing doubles with == | NaN != NaN; 0.0 == -0.0 differs from Double.compare |
instanceof in equals with a stateful subclass | Broken symmetry |
| Mutable key fields | Entries become unreachable after mutation |
equals that throws on null | Violates the contract; Objects.equals avoids it |
Interview angle
- "What is the
equals/hashCodecontract?" Equal objects must have equal hash codes;equalsis reflexive, symmetric, transitive, consistent, false for null. - "What happens if you override
equalsonly?" Hash-based collections fail to find your objects. - "Can two unequal objects have the same
hashCode?" Yes — a collision. - "
getClass()orinstanceofinequals?"getClass()for strict symmetry with subclasses;instanceoffor final classes or stateless subclasses. - "Why are strings good map keys?" Immutable, with cached, content-based hash codes.
Key takeaways
- Override
equals(Object),hashCodeandtoStringtogether for value-like classes; records do it for you. equals: identity fast path, null/type check, field comparison with==/Double.compare/Objects.equals.hashCodefrom the same fields (Objects.hash); equal ⇒ same hash, always.- Keys must be immutable; a mutated key is a lost entry.
- Avoid
clone; write copy constructors.
Common questions
What happens if you override equals but not hashCode in Java?
Hash-based collections stop finding your objects. Two equal objects then usually have different identity-based hash codes, so HashMap.get or HashSet.contains searches the wrong bucket and finds nothing — even for a key you have just added.
How do you write an equals method in Java?
Take an Object parameter; return true if this == o; return false if o is null or getClass() != o.getClass(); then cast and compare the fields — primitives with ==, doubles with Double.compare, references with Objects.equals. Build hashCode from the same fields, for example with Objects.hash.
Should equals use getClass or instanceof in Java?
getClass() makes objects of different classes unequal, which keeps equals symmetric when a subclass adds fields. instanceof lets a subclass equal its parent and is safe only when the class is final or its subclasses add no state.
Can two unequal objects have the same hashCode in Java?
Yes — that is a collision, and the contract allows it. Only the reverse is forbidden: equal objects must have equal hash codes. Collisions slow hash tables down, and a constant hashCode, though legal, puts every entry in the same bucket.
What methods does the Object class have in Java?
equals, hashCode, toString, getClass, clone, finalize, and the thread methods wait, notify and notifyAll. By default equals compares identity and toString prints the class name and hash; getClass and the thread methods are final, and finalize is deprecated for removal.
Exercises
Points in a set
Write final class Point with int x, y, a constructor, and correct equals(Object), hashCode() and toString() ((x, y)). Read an integer n and n pairs x y; add each as a Point to a HashSet<Point>. Print distinct=<set size>, then contains(0,0)=<true|false>.
With a broken equals/hashCode the set would count duplicates separately — the hidden cases check that.
Example: 4 then 1 2, 1 2, 0 0, 2 1 →
distinct=3
contains(0,0)=trueMoney as a map key
Write a final class Money with long cents and String currency, a correct equals(Object), a consistent hashCode and a toString like 12.50 USD. Read an integer n and n lines <cents> <currency>. Count each distinct amount with a HashMap<Money, Integer> and print <money> x<count> in first-seen order (use a LinkedHashMap), then distinct=<size>. Also print equal=<new Money(100,"USD").equals(new Money(100,"USD"))> sameHash=<their hash codes are equal>.
Example: 3 then 1250 USD, 1250 USD, 1250 EUR →
12.50 USD x2
12.50 EUR x1
distinct=2
equal=true sameHash=trueIn this module: OOP II — inheritance & polymorphism
- Extending classes — super, constructors and what is inherited
- Overriding — the rules and the traps
- Polymorphism, casting and instanceof
- The Object class — equals, hashCode and toString (this lesson)
- Abstract classes and the template method
- When not to inherit — composition, Liskov and final
- Checkpoint — Inheritance & polymorphism
← Polymorphism, casting and instanceof · Abstract classes and the template method →