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,equalsandhashCode. - Override
- Give your own version of a method you inherited, with the same name and parameter types.
@Overrideasks 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
intcomputed 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.
equalsandhashCodehave written contracts in theObjectjavadoc.
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.
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.
@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.
@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).
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.
@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
Remove hashCode from Point
In the first example, delete
hashCode()fromPoint. Predict the linehash a == hash b, then run. Equal objects with different hashes now break the contract. - 2
Make the key safe
In the mutable-key example, make
rowandcolfinal. Thec.row = 5;line no longer compiles. That's the point: immutable keys can't get lost. - 3
Overload by mistake
In "Forget hashCode", change
Good.equals(Object o)toequals(Good o)(adjust the body) and keep@Override. Read the compiler error. Then remove@Override, run, and predict the set size.
Code & diagrams
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: 994With identity hashes, two equal NoHash objects (almost) never share a hash, so the set treats them as different.
Expected output
NoHash: equals says true
NoHash: set size 2, contains false
Good: set size 1, contains trueExpected output
before: contains c? true
after: contains c? false
after: contains (1,2)? false
size still 1: [(5,2)]String.hashCode is s[0]*31^(n-1) + ... + s[n-1]: 65*31+97 = 66*31+66 = 2112. Use Double.compare in equals, not ==.
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) = 0Break 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.
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.
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.
Queryobjects are created fresh for each request.- The map is a plain
HashMap(orConcurrentHashMap).
The interviewer follows up
Why did memory keep growing rather than staying flat?
Would ConcurrentHashMap have avoided the problem?
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
HashMapdoesn't usehashCode()raw: it XORs the high 16 bits into the low 16 (h ^ (h >>> 16)) and takeshash & (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 poorhashCodedegrades lookups to O(log n) rather than O(n) since Java 8. Topic 9.4 dissects it. - ▸
Stringcaches 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. - ▸
equalsfor floating-point fields should useDouble.compare(a, b) == 0(as records andDouble.equalsdo), soNaNequals itself and0.0differs from-0.0. Using==makesequalsnon-reflexive forNaNfields, which breaks collections. - ▸
compareToshould be consistent with equals (a.compareTo(b) == 0exactly whena.equals(b)).BigDecimalfamously isn't:new BigDecimal("1.0")and"1.00"are notequalsbut compare as 0, so aHashSetholds both while aTreeSetholds one.
Remember this
- 1
Every class extends
java.lang.Object(directly or through its parents), so every object hastoString(),equals(Object)andhashCode(). The defaults are identity-based:equalsis the same as==,hashCodeis derived from the object's identity, andtoStringprintsClassName@followed by the hash in hex, likePoint@1b6d3586. - 2
**
toString** is called automatically bySystem.out.println(obj), by string concatenation ("p = " + p), and byString.valueOf. Override it to return a short, readable description of the object's state. It's for humans and logs, not for parsing. - 3
**
equalsmust define value equality and obey the contract** (Object.equalsjavadoc): reflexive (x.equals(x)), symmetric (x.equals(y)exactly wheny.equals(x)), transitive (if x equals y and y equals z, x equals z), consistent (same answer while the fields don't change), andx.equals(null)isfalse. Its parameter type must beObject; writingequals(Point p)overloads instead of overriding, and collections never call it. - 4
**
hashCode** has its own contract: ifa.equals(b), thena.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 overrideequals, you must overridehashCode** using the same fields. Break this andHashSet.containsreturnsfalsefor an equal object. - 5
Why: a
HashMap(Topic 9.4) useshashCode()to pick a bucket, then callsequalsonly 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 inhashCodewhile the object is a key**: its stored hash goes stale and the entry becomes unreachable. - 6
The modern recipe:
@Override public boolean equals(Object o)with an identity check, aninstanceoftype check (Java 16's pattern formo instanceof Point psaves the cast), then compare each significant field:==forint/long/char/boolean,Double.compare/Float.comparefor floating point,Objects.equalsfor references that may benull,Arrays.equalsfor arrays.hashCodecombines the same fields withObjects.hash(...)or31 * h + .... Or skip all of it and use a record (Topic 4.9).
Explain it without notes
What do the default toString, equals and hashCode in Object do?
State the five rules of the equals contract.
State the hashCode contract and explain exactly what goes wrong in a HashSet if you override equals without hashCode.
Why is equals(Point p) a bug, and how does @Override help?
Why shouldn't you mutate an object while it's a key in a HashMap?
Practice
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.
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.
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.hashis short and hard to get wrong but allocates an array and boxes primitives per call; hand-written31 * h + ...is faster for hot keys. Records generate an efficient version for you. - ↔
instanceofinequalsallows 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 classesfinalavoids the dilemma. - ↔
A detailed
toStringmakes 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,equalsandhashCodecorrectly from memory, with@Override.Done when you can state both contracts and explain how
HashMaprelies 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.compareand nullable fields withObjects.equalsinequals.Done when you can explain the instanceof vs getClass trade-off.