Command Palette

Search for a command to run...

PHASE 4Beginner ~33 min· topic 8 of 12

Topic 4.8

toString, equals and hashCode

In one line

Every class inherits toString, equals and hashCode from Object. Override toString to get readable output, and override equals and hashCode together, following their contract, whenever two different objects should count as "the same value", because HashMap, HashSet and most of the collections library depend on it.

Think of it like this

Two ₹100 notes. They're different pieces of paper (different objects), yet in a shop they're worth exactly the same (equal values). A cashier doesn't care which note you hand over. And when you sort cash into trays by value, both notes must go into the same tray, or the cashier will look in the wrong tray and say "no ₹100 here". equals is "worth the same"; hashCode picks the tray.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

`Object`
The class at the top of Java's class tree. Every class inherits its methods, including toString, equals and hashCode.
Override
Give your own version of a method you inherited, with the same name and parameter types. @Override asks the compiler to check you got it right.
Identity
Which object something is. Two references have the same identity only if they point to the same object (==).
Value equality
Two objects count as equal because their contents match, even if they're different objects.
Hash code
An int computed from an object, used to decide where to store it in a hash table. Equal objects must have equal hash codes.
Bucket
One slot in a hash table's internal array. Objects whose hash codes map to the same slot share a bucket.
Collision
Two different objects that land in the same bucket (often because they have the same hash code). Allowed, but it slows lookups.
Contract
Rules a method must follow so other code can rely on it. equals and hashCode have written contracts in the Object javadoc.

Step by step

01What you inherit from Object

A class you write with no extends still extends Object. So new Point(1, 2).toString() works even though you never wrote it, and it prints something like Point@1b6d3586: the class name, @, and the identity hash in hex.

The default equals is literally return this == obj;. Two points with the same coordinates are *not* equal by default, because they're two objects.

Object.java (simplified from the JDK)whole filejava
public class Object {
    public boolean equals(Object obj) {
        return (this == obj);
    }

    public native int hashCode();   // identity-based, computed by the JVM

    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
}

02Override toString for readable output

println(p) calls String.valueOf(p), which calls p.toString() (or prints null for a null reference). "at " + p does the same.

Return a compact description like Point(1, 2). Include the fields that identify the object; leave out secrets such as passwords and tokens, because toString ends up in logs.

Main.javawhole filejava
@Override
public String toString() {
    return "Point(" + x + ", " + y + ")";
}

03Write equals the safe way

Step 1, identity: if (this == o) return true; is a fast path and makes reflexivity obvious.

Step 2, type: if (!(o instanceof Point p)) return false; also handles null (null instanceof X is always false) and, with the Java 16 pattern, gives you a typed variable p.

Step 3, fields: compare every field that defines the value. Use == for integer types, Double.compare(a, b) == 0 for doubles (it handles NaN and -0.0 consistently), and Objects.equals(a, b) for references that may be null.

Always put @Override on it. If you accidentally write equals(Point o), the compiler then tells you it doesn't override anything.

Main.javawhole filejava
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;   // Java 16 pattern; also false for null
    return x == p.x && y == p.y;
}

04Why hashCode must match: how a HashSet finds things

A HashSet is a HashMap underneath. To find an object it computes hashCode(), mixes the bits, and uses them to choose one bucket out of (say) 16. Only the keys in that bucket are compared, first by stored hash, then by equals.

If you override equals but not hashCode, two equal points get two unrelated identity hashes. contains(new Point(1, 2)) looks in the wrong bucket, never calls your equals, and returns false. You'll take this machinery apart in Topic 9.4 (HashMap internals).

Why hashCode must match: how a HashSet finds thingsdiagram
Rendering diagram…

05Write hashCode from the same fields

Objects.hash(x, y) (Java 7) combines fields as 31 * (31 * 1 + hash(x)) + hash(y). It's short and correct, but it allocates a varargs array and boxes primitives on every call.

The hand-written form avoids that: int h = Integer.hashCode(x); h = 31 * h + Integer.hashCode(y); return h;. 31 is used because it's an odd prime and 31 * h compiles to a fast shift-and-subtract ((h << 5) - h).

Use exactly the fields equals uses (or a subset). Using a field that equals ignores breaks the contract.

Main.javawhole filejava
@Override
public int hashCode() {
    return Objects.hash(x, y);          // simple, allocates a small array

    // faster, same idea:
    // int h = Integer.hashCode(x);
    // h = 31 * h + Integer.hashCode(y);
    // return h;
}

06instanceof vs getClass() in equals

instanceof accepts subclasses: a Point and a ColorPoint extends Point with the same x and y can be equal. That's fine as long as the subclass doesn't add fields to equality. If ColorPoint also compares colour, symmetry breaks: point.equals(colorPoint) is true but colorPoint.equals(point) is false.

getClass() != o.getClass() makes only exact-same-class objects equal: symmetric, but a subclass can never equal its parent, which breaks code (and proxies) that expect substitutability.

The usual advice (Effective Java, Item 10): make value classes final and use instanceof, or prefer composition over inheritance (Topic 5.12). Records are final, which sidesteps the problem.

07Mutable keys: the silent HashMap bug

A HashMap stores each entry's hash when it's inserted. If you then change a field that hashCode uses, the object now hashes to a different bucket. Looking it up with itself fails, and so does looking up with a copy of the old value (the bucket matches but equals no longer does). The entry is still there, taking memory, but unreachable by lookup.

Rule: objects used as HashMap keys or HashSet elements should be immutable, or at least never change their equality fields while inside. Strings, Integers, records and enums are safe keys. Topic 4.11 covers immutability.

Try it yourself

  1. 1

    Remove hashCode from Point

    In the first example, delete hashCode() from Point. Predict the line hash a == hash b, then run. Equal objects with different hashes now break the contract.

  2. 2

    Make the key safe

    In the mutable-key example, make row and col final. The c.row = 5; line no longer compiles. That's the point: immutable keys can't get lost.

  3. 3

    Overload by mistake

    In "Forget hashCode", change Good.equals(Object o) to equals(Good o) (adjust the body) and keep @Override. Read the compiler error. Then remove @Override, run, and predict the set size.

Code & diagrams

toString, equals and == side by side Java 16+ New tab
Sign in to run this example in your browser.

Expected output

Point(1, 2)
a is Point(3, 4)
a == b:      false
a.equals(b): true
a.equals(c): false
a.equals(null): false
hash a == hash b: true
hash of a: 994
Forget hashCode and HashSet breaks New tab

With identity hashes, two equal NoHash objects (almost) never share a hash, so the set treats them as different.

Sign in to run this example in your browser.

Expected output

NoHash: equals says true
NoHash: set size 2, contains false
Good:   set size 1, contains true
A mutable key gets lost in a HashSet New tab
Sign in to run this example in your browser.

Expected output

before: contains c? true
after:  contains c? false
after:  contains (1,2)? false
size still 1: [(5,2)]
Equal hashes don't mean equal objects New tab

String.hashCode is s[0]*31^(n-1) + ... + s[n-1]: 65*31+97 = 66*31+66 = 2112. Use Double.compare in equals, not ==.

Sign in to run this example in your browser.

Expected output

"Aa".hashCode() = 2112
"BB".hashCode() = 2112
equal? false
Double.compare(0.0, -0.0) = 1
0.0 == -0.0 is true
NaN == NaN is false
Double.compare(NaN, NaN) = 0

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Overload equals instead of overriding it

Write @Override public boolean equals(Point o) { return x == o.x; } with a Point parameter.

terminal
$ javac Main.java
── what you'll see ──
Main.java:1: error: method does not override or implement a method from a supertype
class Point { int x; @Override public boolean equals(Point o) { return x == o.x; } }
^
1 error

Break #2

Override equals but not hashCode

Give a class equals based on its fields and no hashCode, then add two equal objects to a HashSet.

terminal
$ java Main
── what you'll see ──
NoHash: equals says true
NoHash: set size 2, contains false

Myth vs fact

Myth

Equal hash codes mean the objects are equal.

Fact

Only the reverse is guaranteed. Different objects can share a hash code ("Aa" and "BB" both hash to 2112); hash tables then fall back to equals.

Myth

The default hashCode is the object's memory address.

Fact

On HotSpot it's a random-ish number generated on first call and stored in the object header (-XX:hashCode=5, thread-local xorshift by default). Objects move during GC, but their identity hash never changes.

Myth

If my class is never put in a HashMap, I can skip hashCode.

Fact

You don't control how others use your class, and many APIs hash internally (HashSet, distinct() in streams, caches). Always override them together.

Myth

== compares strings by content.

Fact

== compares references for all objects, including strings. Use equals for content (Topic 6.2).

Interview problem

The problem

The cache that never hits

A teammate added an in-memory cache Map<Query, Result> in front of a slow service. Metrics show the hit rate is 0% and memory keeps growing, even though the same queries repeat constantly. Query has fields userId, filter and page. What do you check, and how do you fix it?

You're given

  • Identical queries arrive many times per second.
  • Query objects are created fresh for each request.
  • The map is a plain HashMap (or ConcurrentHashMap).

The interviewer follows up

01

Why did memory keep growing rather than staying flat?

02

Would ConcurrentHashMap have avoided the problem?

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    HashMap doesn't use hashCode() raw: it XORs the high 16 bits into the low 16 (h ^ (h >>> 16)) and takes hash & (n - 1) for the bucket, since the table size is a power of two. Buckets that grow past 8 entries (with a table of at least 64) turn into red-black trees, which is why a poor hashCode degrades lookups to O(log n) rather than O(n) since Java 8. Topic 9.4 dissects it.

  • ▸

    String caches its hash in a field after the first call (and since Java 13 also records whether the hash is genuinely 0), so string keys hash in O(1) after the first time. Caching a hash in your own immutable class is a valid optimisation; in a mutable one it's a bug.

  • ▸

    equals for floating-point fields should use Double.compare(a, b) == 0 (as records and Double.equals do), so NaN equals itself and 0.0 differs from -0.0. Using == makes equals non-reflexive for NaN fields, which breaks collections.

  • ▸

    compareTo should be consistent with equals (a.compareTo(b) == 0 exactly when a.equals(b)). BigDecimal famously isn't: new BigDecimal("1.0") and "1.00" are not equals but compare as 0, so a HashSet holds both while a TreeSet holds one.

Remember this

  1. 1

    Every class extends java.lang.Object (directly or through its parents), so every object has toString(), equals(Object) and hashCode(). The defaults are identity-based: equals is the same as ==, hashCode is derived from the object's identity, and toString prints ClassName@ followed by the hash in hex, like Point@1b6d3586.

  2. 2

    **toString** is called automatically by System.out.println(obj), by string concatenation ("p = " + p), and by String.valueOf. Override it to return a short, readable description of the object's state. It's for humans and logs, not for parsing.

  3. 3

    **equals must define value equality and obey the contract** (Object.equals javadoc): reflexive (x.equals(x)), symmetric (x.equals(y) exactly when y.equals(x)), transitive (if x equals y and y equals z, x equals z), consistent (same answer while the fields don't change), and x.equals(null) is false. Its parameter type must be Object; writing equals(Point p) overloads instead of overriding, and collections never call it.

  4. 4

    **hashCode** has its own contract: if a.equals(b), then a.hashCode() == b.hashCode(). The reverse isn't required: unequal objects may share a hash (a collision), it just makes hash tables slower. So **if you override equals, you must override hashCode** using the same fields. Break this and HashSet.contains returns false for an equal object.

  5. 5

    Why: a HashMap (Topic 9.4) uses hashCode() to pick a bucket, then calls equals only on the few keys in that bucket. Equal objects with different hashes land in different buckets, and the map never even compares them. That's also why you must **never change a field used in hashCode while the object is a key**: its stored hash goes stale and the entry becomes unreachable.

  6. 6

    The modern recipe: @Override public boolean equals(Object o) with an identity check, an instanceof type check (Java 16's pattern form o instanceof Point p saves the cast), then compare each significant field: == for int/long/char/boolean, Double.compare/Float.compare for floating point, Objects.equals for references that may be null, Arrays.equals for arrays. hashCode combines the same fields with Objects.hash(...) or 31 * h + .... Or skip all of it and use a record (Topic 4.9).

Explain it without notes

01

What do the default toString, equals and hashCode in Object do?

02

State the five rules of the equals contract.

03

State the hashCode contract and explain exactly what goes wrong in a HashSet if you override equals without hashCode.

04

Why is equals(Point p) a bug, and how does @Override help?

05

Why shouldn't you mutate an object while it's a key in a HashMap?

Practice

01

Write a final class Money with long paise and String currency, plus correct equals, hashCode and toString. Show that two equal amounts are equals and that a HashSet keeps only one.

02

Write an equals for a class Book(String isbn, String title) where two books are equal if their ISBNs are equal (the title may be null). Show that the null title causes no exception.

03

Show the symmetry trap: a class CaseInsensitive wrapping a String whose equals also accepts plain Strings ignoring case. Print ci.equals("HELLO") and "HELLO".equals(ci) and explain the result.

Trade-offs

  • ↔

    Objects.hash is short and hard to get wrong but allocates an array and boxes primitives per call; hand-written 31 * h + ... is faster for hot keys. Records generate an efficient version for you.

  • ↔

    instanceof in equals allows subclasses to be equal to the parent (substitutable, works with proxies) but risks broken symmetry if subclasses add state; getClass() is strictly symmetric but forbids cross-class equality. Making value classes final avoids the dilemma.

  • ↔

    A detailed toString makes logs and debugging much easier, but it can leak sensitive data (passwords, tokens, personal data) into logs. Mask or omit sensitive fields.

Done when you can

  • Done when you can write toString, equals and hashCode correctly from memory, with @Override.

  • Done when you can state both contracts and explain how HashMap relies on them (preview of Topic 9.4).

  • Done when you can demonstrate the missing-hashCode bug and the mutable-key bug.

  • Done when you compare doubles with Double.compare and nullable fields with Objects.equals in equals.

  • Done when you can explain the instanceof vs getClass trade-off.