skip to content

If a class overrides equals() and hashCode() to compare by value, how can you still test for true object identity, and where does System.identityHashCode fit in?

level: seniorimportance: nice to knowfreq 30%

answer

  1. == can't be overridden → always identity
  2. System.identityHashCode = the default Object hash, bypassing override
  3. identityHashCode shows in default toString (Class@hex), null→0, not unique
  4. IdentityHashMap keys by == not equals()
  5. use identity map for cycle detection / deep copy / per-instance metadata

basics

~20 s

Use == to test whether two references point to the same object — overriding equals() never changes what == does. System.identityHashCode(x) gives the original identity-based hash even when hashCode() is overridden, and IdentityHashMap compares keys by ==.

solid answer

~50 s

Overriding equals()/hashCode() only changes those methods; the == operator and the identity hash are language-level and remain available. So to ask "are these the same object?" you always use ==, regardless of how equals() is defined. When you need the identity hash that Object.hashCode would have returned — for diagnostics, or because the class overrode hashCode() — call System.identityHashCode(obj); it's also what the default toString (ClassName@hex) prints. For collections that must key on identity rather than value (e.g. tracking which exact objects you've visited, serialization graphs, avoiding accidental value-collapse), use java.util.IdentityHashMap, which uses == and identityHashCode instead of equals()/hashCode(). This matters in cases like deep-copy or cycle detection where two equal-by-value objects must still be treated as distinct. The key mental model: equals()/hashCode() define value semantics for your type; ==, identityHashCode, and IdentityHashMap give you back the underlying identity semantics when you specifically need them.

code

java · 15 lines
java
record Point(int x, int y) {}

Point a = new Point(1, 2);
Point b = new Point(1, 2);

System.out.println(a.equals(b)); // true  (value equality)
System.out.println(a == b);      // false (distinct objects)

System.out.println(System.identityHashCode(a)
                != System.identityHashCode(b)); // typically true

Map<Point, String> seen = new IdentityHashMap<>();
seen.put(a, "first");
System.out.println(seen.containsKey(b)); // false (b is a different object)
System.out.println(seen.containsKey(a)); // true

go deeper

for a junior

Knows == still tests identity even if equals() is overridden.

for a middle

Can use System.identityHashCode for diagnostics and explains the default toString hex; aware HashMap uses value equality.

for a senior

Reaches for IdentityHashMap in cycle-detection/deep-copy scenarios and explains why value equality would be wrong there; knows identityHashCode caveats (collisions, not an address).

for a principal

Designs object-graph processing (serialization, cloning, interning) deliberately choosing identity vs value semantics, and understands JVM costs/locking interactions of the identity hash (e.g. its relationship to object headers).

## The setup Suppose you write a value class and override `equals()`/`hashCode()` so that two distinct objects with the same data are *equal*: ```java record Point(int x, int y) {} Point a = new Point(1, 2); Point b = new Point(1, 2); a.equals(b); // true (value equality) a == b; // false (still two different objects) ``` Sometimes you still need to ask the **identity** question — *are these literally the same object?* — even though `equals()` now says they're equal. Java keeps three identity tools that overriding `equals`/`hashCode` does **not** touch. ## 1. `==` is unaffected by overrides The `==` operator is a **language operator**, not a method, so it can't be overridden. No matter what `equals()` does, `a == b` always tests reference identity. This is your primary identity test. ## 2. `System.identityHashCode(obj)` `Object.hashCode()`'s default returns an **identity hash** — a value derived from the object's identity, not its fields. When you override `hashCode()`, you replace that default, so you can no longer get the identity hash via `obj.hashCode()`. `System.identityHashCode(obj)` returns *the value `Object.hashCode()` would have returned* for that object, **bypassing any override**: ```java System.identityHashCode(a); // identity hash, ignores Point's hashCode() ``` Properties to know: - It's the number you see in the default `toString`: `ClassName@` + `Integer.toHexString(identityHashCode)`. - `identityHashCode(null)` returns `0`. - It is **not** a memory address and **not** guaranteed unique — two different objects can share an identity hash (collisions are possible). Use it for diagnostics, not as a unique id. ## 3. `IdentityHashMap` Normal `HashMap`/`HashSet` key on **value** semantics: they call your `equals()` and `hashCode()`. Sometimes you need a map/set keyed on **identity** — treating two equal-by-value objects as **different** keys. `java.util.IdentityHashMap` does exactly that: it compares keys with `==` and hashes with `identityHashCode`. Classic uses: - **Graph traversal / cycle detection**: "have I visited *this exact* node?" — value equality would wrongly merge distinct-but-equal nodes. - **Deep copy / serialization**: map each original object to its copy, preserving shared structure and breaking cycles. - **Per-instance side tables**: associating metadata with specific object instances. ```java Map<Object, String> seen = new IdentityHashMap<>(); seen.put(a, "A"); seen.containsKey(b); // false — b is a different object, even though a.equals(b) seen.containsKey(a); // true ``` (`Collections.newSetFromMap(new IdentityHashMap<>())` gives an identity-based Set.) ## Mental model - `equals()`/`hashCode()` = the **value** semantics *you* define for the type. - `==`, `System.identityHashCode`, `IdentityHashMap` = the **identity** semantics the platform always keeps underneath, available when value semantics are the wrong question. This layered design means overriding equality for convenience never *loses* the ability to reason about object identity when you genuinely need it.

  • Why might a graph algorithm need IdentityHashMap instead of HashMap?
    Two distinct nodes can be equal by value (same payload). A normal HashMap would treat them as one key, corrupting visited-tracking or cycle detection. IdentityHashMap keys by ==, so each distinct object is a separate key, which is what graph traversal needs.
  • After overriding hashCode(), how do you still get the original identity hash?
    Call System.identityHashCode(obj). It returns the value Object.hashCode() would have produced, ignoring your override — useful for logging/diagnostics and for understanding the default toString output.

saying these in an interview costs you the question

  • Thinking overriding equals() changes what == does
  • Treating identityHashCode as a unique id or memory address
  • Using a normal HashMap when you need identity keys (merges equal objects)
  • Believing identityHashCode never collides
  • Assuming you can override the == operator

context