When is === (referential equality) the right tool, and what are the risks of relying on it — for example with boxed Int caching?
answer
- === = same instance, never calls equals()
- Use for cache/identity/cycle detection and equals() fast path
- Boxed Int cache -128..127 -> 127===127 true, 128===128 false
- String interning can make === accidentally true
- Value -> ==, identity -> ===
basics
~20 sUse === when you truly care about object identity: checking something is the same instance, like a cache hit or breaking a reference cycle. Avoid it for value comparison because identical-looking values may or may not be the same object.
solid answer
~40 s`===` compares **identity** — the same object instance — and never calls `equals()`. Legitimate uses: confirming a cache/interning returned the *same* object, detecting that a reference was reused, cycle detection in graph traversal, or fast-path `if (this === other) return true` inside an `equals()` override. The risk is using it for value comparison. On the JVM, boxing an `Int` into `Int?`/`Any` caches small values (typically -128..127, the `Integer` cache), so two boxed `127`s may be `===` while two boxed `128`s are not — making `===` results depend on the value's magnitude. Likewise String literals may be interned. So `===` is non-deterministic for values and must never gate business logic. Rule: `==` for value, `===` for identity, and never the other way around.
code
kotlin · 8 linesval a: Int? = 127
val b: Int? = 127
println(a === b) // true (Integer cache)
val c: Int? = 128
val d: Int? = 128
println(c === d) // false (outside cache)
println(c == d) // true (structural, correct)go deeper
Knows === means 'same object' and should use == for values.
Can give a valid use of === (e.g. equals() fast path) and warns it never calls equals().
Explains the boxed Integer cache making === value-dependent and lists sound identity use cases like cycle detection.
Treats identity comparison as an architectural smell in domain logic, reasoning about structural sharing, interning guarantees, and when identity is a legitimate part of an API contract.
## What === actually does `===` (and its negation `!==`) is **referential equality**: it returns `true` only when both operands reference the **same object instance** in memory. It **never** calls `equals()`. For non-nullable primitive types backed by JVM primitives (a plain `Int`), `===` compares the underlying value. ## Legitimate uses of === - **Identity / cache checks** — verifying that a memoization layer returned the *same* object rather than a new equal one. - **Cycle / visited detection** — graph or tree traversal where you track *the same node*, not an equal-valued one. - **Fast path in equals()** — the conventional first line: ```kotlin override fun equals(other: Any?): Boolean { if (this === other) return true // identity shortcut if (other !is Foo) return false return id == other.id } ``` - **Detecting unchanged state** — e.g. in reactive code, `if (newState === oldState) skipUpdate()` when you guarantee structural sharing. ## The boxed-Int caching trap When an `Int` is **boxed** (assigned to `Int?`, `Any`, or a generic `T`), the JVM may reuse cached `Integer` objects for a small range (commonly **-128..127**). So: ```kotlin val a: Int? = 127 val b: Int? = 127 println(a === b) // true (cached, same object) val c: Int? = 128 val d: Int? = 128 println(c === d) // false (outside cache, new objects) println(c == d) // true (structural, always correct) ``` The `===` result **depends on the literal's magnitude** — a clear sign it is the wrong tool for comparing values. String literals can be interned similarly, making `===` accidentally `true`. ## The rule - **Value comparison -> `==`** (structural, calls `equals()`, null-safe). - **Identity comparison -> `===`** (same instance), and only when identity is genuinely the requirement. Never let `===` drive business logic on values; its truth can change with boxing, caching, or interning details that are implementation-defined.
- Why does 127 === 127 differ from 128 === 128 when boxed?The JVM Integer cache reuses one object for small values (typically -128..127), so boxed 127s share an instance; 128 falls outside the cache and creates new objects each time.
- Where is === idiomatic inside an equals() override?As the first line: if (this === other) return true — a cheap identity short-circuit before the more expensive structural comparison.
=== is asking 'is this the same physical key?'; == is asking 'does this key open the same lock?'
saying these in an interview costs you the question
- Using === to compare values like ids or numbers
- Assuming === on equal values is deterministic
- Not knowing about the Integer/box cache
- Thinking === ever calls equals()
- Relying on String interning for correctness