How does the Kotlin compiler lower a?.b?.c to JVM bytecode/Java, and what are the receiver-evaluation and short-circuit guarantees a senior should rely on?
answer
- lowers to: tmp = receiver; if null -> null else tmp.member
- receiver evaluated exactly once (temporary)
- chain = nested null checks, stop at first null
- side effects after a null don't run
- pure compiler sugar, just branches, no runtime cost
basics
~20 sUnder the hood, a?.b becomes a null check: evaluate the receiver once, if it's null give null, otherwise read the member. Chained calls become nested checks that stop at the first null. The receiver is only computed one time.
solid answer
~40 sThe compiler lowers receiver?.member to roughly: val tmp = receiver; if (tmp == null) null else tmp.member. The receiver is stored in a temporary and evaluated exactly once — important when the receiver is a function call with side effects, e.g. compute()?.foo() calls compute() once. Chained safe calls a?.b?.c nest these checks and short-circuit: the first null aborts evaluation and the result is null, so later members/calls and their side effects don't execute. The whole expression's type is the final member type made nullable. This is why you can't assume later links run, and why mixing side-effecting calls into long safe-call chains is risky. It's pure compiler sugar — no reflection, just null-comparison branches in bytecode.
code
kotlin · 5 lines// compute() runs once even though value is checked and used
val n = compute()?.length
// equivalent to:
val tmp = compute()
val n2 = if (tmp == null) null else tmp.lengthgo deeper
Understands ?. avoids NPE but not the lowering details.
Knows the chain short-circuits and the result is nullable.
Explains the temporary/single-evaluation guarantee and nested-check lowering, and its side-effect implications.
Uses the evaluation-once and short-circuit guarantees to reason about API ergonomics and to flag risky side-effecting chains in reviews.
## How `?.` is compiled The safe call is **syntactic sugar** the Kotlin compiler lowers to ordinary null-comparison branches — no reflection, no special runtime support. ### Single safe call `receiver?.member` lowers to approximately: ```kotlin // conceptual lowering of: receiver?.member val tmp = receiver if (tmp == null) null else tmp.member ``` Two guarantees follow: 1. **Receiver evaluated exactly once.** It's captured in a temporary (`tmp`). So `compute()?.length` calls `compute()` a single time, even though the value is both null-checked and dereferenced. 2. **Member access is conditional.** `.member` runs only in the non-null branch. ```kotlin fun compute(): String? { println("called"); return null } val n = compute()?.length // prints "called" once; n == null ``` ### Chained safe calls `a?.b?.c` lowers to **nested** checks, short-circuiting at the first null: ```kotlin // conceptual lowering of: a?.b?.c val ta = a if (ta == null) null else { val tb = ta.b if (tb == null) null else tb.c } ``` Guarantees a senior relies on: - **Short-circuit:** the first null aborts the rest; later members and their **side effects do not run** (`a?.foo()?.bar()` skips `bar()` if `foo()` returns null). - **Single evaluation per link:** each receiver in the chain is evaluated once. - **Result type:** the last member's type, made nullable (`C?`). ### Practical implications - Putting side-effecting calls deep in a chain is risky because they may silently not run. - Because the receiver is evaluated once, you can safely use an expensive/side-effecting receiver without recomputing it. - The lowering compiles to plain branch instructions (`ifnull`-style), so there's **no performance penalty** beyond a null comparison. ### Relation to smart casts Inside the non-null branch the receiver is known non-null, which is the same mechanism that powers smart casts after a `!= null` check. `?.` essentially inlines that check at the call site.
- If the receiver of a safe call is a function with side effects, how many times does it run?Exactly once: the compiler captures it in a temporary before the null check and reuse.
- Is there a runtime performance cost to ?. compared with a manual null check?No meaningful cost; it lowers to the same null-comparison branch you'd write by hand.
saying these in an interview costs you the question
- Claiming ?. uses reflection or special runtime machinery
- Saying the receiver expression is evaluated twice
- Believing side-effecting calls after a null still execute
- Thinking ?. has a significant performance overhead
- Not knowing the chain short-circuits at the first null