AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

08 OOP II — inheritance & polymorphism / Lesson 4 of 6

The Object class — equals, hashCode and toString

16 min · +10 XP · 2 exercises · 5 questions

Read the text16 min
Exercises0/2
Quiz5 q

Quick answer

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.

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

MethodDefault 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, notifyAllThread 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) and y.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 that HashSet, List.contains and Objects.equals never call.
  • getClass() != o.getClass() makes equals false across different classes, including subclasses — which keeps symmetry when a subclass adds fields. The alternative o instanceof Point lets subclasses be equal to parents and needs care (a ColorPoint equal to a Point but not vice versa violates symmetry). Use instanceof only when subclasses add no state or when the class is final.
  • Compare primitives with ==, double/float with Double.compare(a, b) == 0 (handles NaN and −0.0), references with Objects.equals(a, b) (null-safe), arrays with Arrays.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

MistakeConsequence
equals(Point) instead of equals(Object)Overload; collections use identity
equals without hashCodeLost in hash-based collections
hashCode using fields equals ignoresEqual objects, different hashes
Comparing doubles with ==NaN != NaN; 0.0 == -0.0 differs from Double.compare
instanceof in equals with a stateful subclassBroken symmetry
Mutable key fieldsEntries become unreachable after mutation
equals that throws on nullViolates the contract; Objects.equals avoids it

Interview angle

  • "What is the equals/hashCode contract?" Equal objects must have equal hash codes; equals is reflexive, symmetric, transitive, consistent, false for null.
  • "What happens if you override equals only?" Hash-based collections fail to find your objects.
  • "Can two unequal objects have the same hashCode?" Yes — a collision.
  • "getClass() or instanceof in equals?" getClass() for strict symmetry with subclasses; instanceof for final classes or stateless subclasses.
  • "Why are strings good map keys?" Immutable, with cached, content-based hash codes.

Key takeaways

  • Override equals(Object), hashCode and toString together for value-like classes; records do it for you.
  • equals: identity fast path, null/type check, field comparison with ==/Double.compare/Objects.equals.
  • hashCode from 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.

Mark as readSign in to keep your place.

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

MethodDefault 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, notifyAllThread 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) and y.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 that HashSet, List.contains and Objects.equals never call.
  • getClass() != o.getClass() makes equals false across different classes, including subclasses — which keeps symmetry when a subclass adds fields. The alternative o instanceof Point lets subclasses be equal to parents and needs care (a ColorPoint equal to a Point but not vice versa violates symmetry). Use instanceof only when subclasses add no state or when the class is final.
  • Compare primitives with ==, double/float with Double.compare(a, b) == 0 (handles NaN and −0.0), references with Objects.equals(a, b) (null-safe), arrays with Arrays.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

MistakeConsequence
equals(Point) instead of equals(Object)Overload; collections use identity
equals without hashCodeLost in hash-based collections
hashCode using fields equals ignoresEqual objects, different hashes
Comparing doubles with ==NaN != NaN; 0.0 == -0.0 differs from Double.compare
instanceof in equals with a stateful subclassBroken symmetry
Mutable key fieldsEntries become unreachable after mutation
equals that throws on nullViolates the contract; Objects.equals avoids it

Interview angle

  • "What is the equals/hashCode contract?" Equal objects must have equal hash codes; equals is reflexive, symmetric, transitive, consistent, false for null.
  • "What happens if you override equals only?" Hash-based collections fail to find your objects.
  • "Can two unequal objects have the same hashCode?" Yes — a collision.
  • "getClass() or instanceof in equals?" getClass() for strict symmetry with subclasses; instanceof for final classes or stateless subclasses.
  • "Why are strings good map keys?" Immutable, with cached, content-based hash codes.

Key takeaways

  • Override equals(Object), hashCode and toString together for value-like classes; records do it for you.
  • equals: identity fast path, null/type check, field comparison with ==/Double.compare/Objects.equals.
  • hashCode from 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)=true

Money 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=true

In this module: OOP II — inheritance & polymorphism

← Polymorphism, casting and instanceof · Abstract classes and the template method →