skip to content

How does the Kotlin compiler translate a == b, and why does that make == null-safe?

level: middleimportance: must knowfreq 70%

answer

  1. a == b -> a?.equals(b) ?: (b === null)
  2. ?. guards null receiver
  3. ?: Elvis supplies b === null fallback
  4. null == null is true, never NPE
  5. equals() override only sees non-null this

basics

~20 s

The compiler turns a == b into a null-check plus equals(). If a is null it doesn't call equals(); it just checks whether b is also null. So == never crashes on a null value.

solid answer

~40 s

Kotlin desugars `a == b` to `a?.equals(b) ?: (b === null)`. The safe call `?.` means: if `a` is null, skip `equals()` and yield null, then the Elvis operator `?:` returns `b === null`. If `a` is non-null, it calls `a.equals(b)`. Result: `null == null` is true, `null == something` is false, and there is never a `NullPointerException` from the operator itself. This differs from Java where `a.equals(b)` throws when `a` is null. One consequence: your overridden `equals()` is only invoked on a non-null receiver, so inside `equals()` you don't have to handle a null `this`, but you must still handle a null/other-type `other` argument (typically via `if (other !is MyType) return false`).

code

kotlin · 6 lines
kotlin
fun describe(a: String?, b: String?) = when {
    a == b -> "equal"      // null==null -> true, no NPE
    else   -> "different"
}
println(describe(null, null)) // equal
println(describe(null, "x"))  // different

go deeper

for a junior

Knows == won't crash on null and that null == null is true, even if unsure of the exact desugaring.

for a middle

Reproduces a?.equals(b) ?: (b === null) and explains each operator's role in null-safety.

for a senior

Connects desugaring to how equals() overrides should be written and why the other !is Type pattern suffices.

for a principal

Reasons about API ergonomics: == removing NPE-prone Objects.equals, and the contract obligations equals/hashCode impose on shared collection keys.

## The desugaring Whenever you write `a == b`, the Kotlin compiler generates code equivalent to: ```kotlin a?.equals(b) ?: (b === null) ``` Let's unpack the two operators involved: - **`?.` (safe call)** — `a?.equals(b)` calls `equals()` only if `a` is non-null; if `a` is null the whole expression short-circuits to `null` **without** invoking anything. - **`?:` (Elvis operator)** — `x ?: y` returns `x` if it is non-null, otherwise `y`. Here the fallback `b === null` (a referential check) handles the case where `a` was null. ## Truth table that falls out of it | a | b | a == b | |---|---|--------| | null | null | `true` (Elvis returns `null === null`) | | null | non-null | `false` | | non-null | anything | result of `a.equals(b)` | ## Why it is null-safe Because the safe call guards the receiver, the operator can never dereference a null `a`. In **Java**, `a.equals(b)` throws `NullPointerException` if `a` is null; Kotlin's `==` removes that entire class of bug. You never need `Objects.equals(a, b)` in Kotlin. ## Implication for writing equals() Since `equals()` is only ever called on a non-null receiver via `==`, inside your override you don't handle a null `this`. You **do** handle the parameter: ```kotlin class Money(val cents: Long) { override fun equals(other: Any?): Boolean { if (this === other) return true // fast identity path if (other !is Money) return false // null or wrong type -> false return cents == other.cents } override fun hashCode(): Int = cents.hashCode() } ``` The `other !is Money` smart-cast check returns false for both `null` and any non-`Money`, satisfying the `equals()` contract. ## Note on != `a != b` desugars analogously to `!(a == b)`, preserving the same null-safety.

  • Do you still need to handle null inside your overridden equals()?
    Only the `other` parameter. The receiver `this` is guaranteed non-null because == routes through a safe call. An `other !is Type` check covers a null argument and wrong types at once.
  • What is the Java equivalent that == replaces?
    Objects.equals(a, b) — Kotlin's == gives the same null-tolerant behavior built into the operator.

saying these in an interview costs you the question

  • Claiming == calls equals() even when the left operand is null
  • Saying you must null-check this inside an overridden equals()
  • Confusing ?. (safe call) with !! (not-null assertion)
  • Thinking == on nulls throws
  • Not knowing the Elvis fallback is a referential === check

context