Core Libraries

equals & hashCode

The default equals and hashCode compare by identity, so two objects with equal values aren't "equal", and they misbehave in hash-based collections. Override equals to define value equality, but you MUST override hashCode to agree. Otherwise a HashSet or HashMap silently fails to find your object and stores duplicates. Covers the contract, how it breaks, the equals(Point) overload trap, the mutated-key trap and the Objects.hash fix, all with verified output.

Suggest an edit

equals & hashCode — the Contract Behind Hash Collections

Back in References, Equality & the Object Model, you saw that == compares identity, while .equals compares meaning — if the class defines what "meaning" is. Out of the box, it doesn't. Every class inherits an equals that checks identity (is this the same object?), along with a hashCode that is based on identity too. That means two Point(1, 2) objects are not equal, and they misbehave in the hash-based collections from the last lesson.

The fix is to override equals so that it compares values. But that fix comes with a trap, and the trap is the reason this lesson exists:

  • if you override equals, you must also override hashCode to match;
  • if you don't, HashSet and HashMap silently fail to find your objects — and happily let duplicates in.

Think of the two methods as a contract. Honoring one half without the other is worse than honoring neither.

💡 The core idea.

  • By default, equals/hashCode compare by identity, so value-equal objects aren't equal.
  • Override equals to get value equality — but you must override hashCode to match.
  • Break that contract, and HashSet/HashMap silently miss your objects and let duplicates in.

Every output below was produced by compiling and running the code on Java 25.

You'll be able to: predict what == and equals return for two objects, both with and without an equals override; write an equals(Object) with @Override, and explain why collections ignore an equals(Point); state the hashCode contract, and predict what contains and size return on a HashSet when that contract is broken; write hashCode with Objects.hash from the same fields, and explain why changing a field of an object inside a HashSet loses it.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the compiler and the JVM do.
  2. A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. Default equals is identity
  2. Overriding equals for value equality
  3. The hashCode contract
  4. Generating both correctly
  5. Mental-model summary
  6. Gotcha checklist
  7. Check yourself
  8. Sources

1. Default equals is identity

If a class doesn't define its own equals, it inherits the one from Object. That version returns true only when both references point at the same object [1]. So two objects created separately are not equal, even when their fields are identical:

Output:

false
false

Analysis. a == b is false — no surprise, since these are two distinct objects. But a.equals(b) is false too. The inherited equals is an identity check, so by default .equals and == give the same answer. The class never said what it means for two points to be "equal", so Java falls back on the safest default: a thing equals only itself.

Intuition. Mechanism. For non-null x and y, Object.equals returns true "if and only if x and y refer to the same object" [1]. So unless a class overrides it, "equal" simply means "the same object".

Concrete bite. That second false is the surprise, because most people expect .equals to compare contents. And it matters beyond your own calls, because the collections use .equals too. List.contains looks for an element e "such that Objects.equals(o, e)" [2], and Set membership and Map key lookups work the same way. So every one of them treats your two equal points as different.

💡 Earned rule. Override equals when a class represents a value — a point, an amount of money, a name — and two instances with the same fields should count as equal. The default identity behavior is right only for objects whose identity is their meaning.

The cost is writing and maintaining the method. The payoff is that equality means what your domain says it means, not "same allocation".


2. Overriding equals for value equality

To define value equality, override equals(Object o). The recipe has two steps: check that the argument is a Point, then compare the fields.

  • o instanceof Point tests the type, and it returns false for null.
  • (Point) o casts the argument, so you can reach its fields.

Later, in Sealed Classes & Pattern Matching, you'll meet pattern matching for instanceof, which fuses these two steps into one.

Output:

false
true

Analysis. Now a.equals(b) is true: the overridden method compared x and y, and they match. a == b is still false, because == checks identity, and these are still two separate objects. The two notions have now parted ways: == asks "same object?", while .equals asks "same value?".

The @Override annotation asks the compiler to confirm that this method really does override one from a supertype [3]. For equals, that means the parameter type must be Object.

Intuition. Mechanism. Once you override equals, your comparison replaces the identity check everywhere .equals(Object) is called — including deep inside the collections. The instanceof guard returns false for null and for the wrong type. That satisfies the contract's rule that x.equals(null) returns false [1]. The contract also asks for a relation that is reflexive, symmetric, transitive and consistent. Comparing the same fields on both sides gives you all four.

Concrete bite. Here's a subtle slip: public boolean equals(Point p), with a Point parameter instead of Object. This does not override Object.equals — it overloads it. The JLS calls out this exact mistake as the classic reason @Override exists [3]. Add @Override, and javac rejects it:

Compiler error:

Main.java:5: error: method does not override or implement a method from a supertype
    @Override
    ^

Leave out @Override, though, and the same class compiles — and the bug goes silent. This version even has a correct hashCode:

Output:

true
false
false

Analysis. The compiler chooses an overload based on the argument's declared type:

  • a.equals(b): b is declared as Point, so this calls the new equals(Point), which returns true.
  • a.equals(o): o is declared as Object, so this calls the inherited equals(Object). That's the identity check, so it returns false.
  • HashSet calls equals(Object), so contains(b) is false, even though both hash codes match.

💡 Earned rule. Override equals(Object o) — with that exact signature, plus @Override — and compare the fields that define value equality. Guard the type with instanceof.

The cost is some care with the signature and with the fields you include. The payoff is value semantics everywhere .equals is used — but only if you also fix hashCode, which is exactly what the next section insists on.


3. The hashCode contract

Here's the rule that makes or breaks hash collections [4]:

  • If a.equals(b) is true, then a.hashCode() must equal b.hashCode().
  • Unequal objects may share a hash code. Distinct codes just make hash tables faster.
  • An object's hash code must stay the same for as long as the fields that equals uses stay the same.

The Object.equals documentation adds: "It is generally necessary to override the hashCode method whenever this method is overridden" [1]. Here's what happens if you ignore that advice. Override equals but keep the inherited, identity-based hashCode, and equal objects end up with different hash codes. A HashSet or HashMap then looks in the wrong bucket, and your object effectively vanishes.

Output:

false
2
Point(1,2) — object Aidentity hashCode → 7 (example)Point(1,2) — object Bidentity hashCode → 42 (example)HashSet buckets0: (empty)1: (empty)2: B3: A 7 % 4 = 342 % 4 = 2

Analysis. This code is broken in two visible ways:

  • contains(new Point(1, 2)) is false. The set computed the new point's identity hash code, went to that bucket, and didn't find the stored point — because the stored point lives in a different bucket.
  • Adding a second equal point grew the size to 2. The set put the newcomer in its own bucket and never noticed the duplicate.

The diagram shows why, using example numbers. Identity hash codes ignore the fields entirely, so value-equal points scatter into different buckets. Your equals never even gets consulted, because the set never looks in the right bucket.

Intuition. Mechanism. A hash collection finds an object in two steps: first it hashes to a bucket, then it calls .equals within that bucket. If equal objects have unequal hash codes, step one sends them to different buckets, and step two never runs. Your equals is correct — it's just unreachable.

Concrete bite. That false and that 2 are the breakage: an element that is present reports as absent, and a duplicate slips in. Worse, the class looks right, because equals works fine on its own. It fails only inside hash collections, which makes this one of the hardest Java bugs to spot.

💡 Earned rule. Whenever you override equals, override hashCode in the same change, so that equal objects always produce equal hashes. Never one without the other.

The cost is writing a second method. The cost of skipping it is a class that passes its a.equals(b) tests, yet silently corrupts every HashSet or HashMap it is used in.


4. Generating both correctly

The good news: you rarely need to write hashCode by hand. Two helpers do the heavy lifting:

  • Objects.hash(...) builds a hash from the fields you pass, "as if all the input values were placed into an array" and hashed [5];
  • Objects.equals(a, b) compares two fields safely, even when either one may be null.

Pass exactly the fields that equals compares, and the contract holds.

Output:

true
1

Analysis. Now that hashCode is computed from x and y, two equal points hash to the same bucket. So contains finds the stored point (true), and the duplicate is recognized and dropped (size stays 1). Every link in the chain holds: equal points produce equal hashes, equal hashes lead to the same bucket, and equals confirms the match.

Intuition. Mechanism. Objects.hash(x, y) turns the same field values into the same int, every time. equals then confirms the match within the bucket. The key is that both methods use the same fields. A field in hashCode but not in equals breaks the contract all over again: two objects can be equal, yet hash to different buckets. The reverse is safe but costly. A field in equals but not in hashCode keeps the contract, but it weakens the hash, so more unequal objects share a bucket.

Concrete bite. The contract also asks that a hash code stay put while the object is in use. Change a field that hashCode depends on after the object is already in a HashSet, and the set loses track of it [6]:

Output:

false
false
1

Analysis. The point still sits in the bucket for (1, 2), but it now hashes as (5, 2):

  • contains(p) looked in the bucket for (5, 2) and found nothing there.
  • contains(new Point(1, 2)) found the right bucket, but equals reported that the stored point is now (5, 2).
  • size() is still 1. The object is in the set — it's just unreachable by lookup.

This isn't a bug in HashSet. The Set API warns that its behavior "is not specified if the value of an object is changed in a manner that affects equals comparisons" [6], and the Map API says the same about keys [7].

💡 Earned rule. Generate equals and hashCode together, from the same fields — with Objects.equals and Objects.hash, or your IDE's generator — and keep them in sync whenever the fields change. Never change those fields while the object is in a hash collection. Better still, make them final.

The cost is boilerplate that has to stay consistent. The payoff is correctness in every hash collection. And a record removes the cost entirely: it generates equals, hashCode and toString from its components, which are final.


5. Mental-model summary

Principle Consequence
The default equals/hashCode are identity-based Value-equal objects compare false and don't dedup in collections
Override equals(Object) (with @Override) for value equality equals(Point) only overloads — collections ignore it; == stays identity
Contract: equal objects must have equal hash codes equals without hashCode breaks HashSet/HashMap
A hash collection hashes to a bucket, then .equals within it Wrong hash → wrong bucket → present object reported absent, duplicates added
Generate both from the same fields (Objects.hash/equals) Consistent hash + equality; a record generates them for free
A hash code must not change while the object is in a hash collection Changing a hashed field loses the object; keep those fields final

6. Gotcha checklist

Symptom Likely cause Fix
a.equals(b) is false for two objects with the same fields the class does not override equals override equals(Object)
A HashSet or HashMap can't find an object it contains equals overridden, hashCode not add a hashCode over the same fields
Duplicates appear in a Set of "equal" objects the same inconsistent hashCode override it to match equals
method does not override or implement a method from a supertype on equals the parameter type is not Object declare equals(Object o)
Your equals works directly but collections ignore it equals(YourType) overloads instead of overriding equals(Object o) with @Override
Equal objects hash differently equals and hashCode use different fields use the same fields in both
An object in a HashSet is no longer found after an update a field used by hashCode changed while it was in the set never change those fields; remove, change, re-add; or make them final
Lots of boilerplate to keep in sync hand-written methods Objects.equals/Objects.hash, an IDE generator, or a record

✅ Check yourself

One check per objective. Answer before you open anything.

The 🧪 box below: the four predictions.
  • new Point(1,2).equals(new Point(1,2)) with no overrides: false — the §1 proof.
  • With equals but no hashCode: contains is false and size() is 2 — the §3 proof.
  • With hashCode from Objects.hash(x, y): contains is true and size() is 1 — the §4 proof.
  • With equals(Point) instead of equals(Object): the set still calls equals(Object), which is the inherited identity check. Matching hash codes lead the set to the right bucket, but identity says "different", so contains is false — the §2 proof.

📚 Sources

  1. java.lang.Object.equals(Object), Java SE 25 API (identity; the equivalence-relation contract; "It is generally necessary to override the hashCode method") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#equals(java.lang.Object)
  2. java.util.List.contains(Object), Java SE 25 API ("such that Objects.equals(o, e)") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/List.html#contains(java.lang.Object)
  3. The Java Language Specification, Java SE 25, §9.6.4.4 "@Override" (the equals(Foo) example) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.4.4
  4. java.lang.Object.hashCode(), Java SE 25 API (the general contract of hashCode) — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.html#hashCode()
  5. java.util.Objects.hash(Object...) and Objects.equals(Object, Object), Java SE 25 API — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Objects.html#hash(java.lang.Object...)
  6. java.util.Set, Java SE 25 API ("Great care must be exercised if mutable objects are used as set elements") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Set.html
  7. java.util.Map, Java SE 25 API ("great care must be exercised if mutable objects are used as map keys") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Map.html

🧪 Predict, then check.

  1. For the §1 Point (no overrides), predict new Point(1,2).equals(new Point(1,2)).
  2. Add a correct equals but no hashCode. Put one Point(1,2) in a HashSet, then predict both contains(new Point(1,2)) and size() after you add a second equal point.
  3. Add hashCode via Objects.hash(x, y), and predict the same two values again.
  4. Explain why overriding equals with a Point parameter (instead of Object) would leave the set broken, even with a hashCode.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionfd26460