Kotlin Fundamentals
Kotlin's everyday syntax looks like a lighter Java — and that is precisely why it gets misread. Behind every "merely convenient" construct sits a specific compiler decision, and the interview question is always about that decision. val freezes the reference, not the object, and those are different things. if and when are not statements but expressions with a value, from which both the absence of a ternary operator and the exhaustiveness requirement on an expression-position when follow immediately. A string template is not interpreted at runtime — the compiler lowers it into concatenation, so it is expensive only in appearance.
The second layer of the topic covers types Java simply does not have. Unit is not void: it is a real type with exactly one value, which is why a lambda with no result is typed () -> Unit, while void cannot be substituted as a generic argument at all. Any is the root of the non-nullable part of the hierarchy (the true top is Any?), and — contrary to the popular answer — wait/notify are not its members. The third layer is what the compiler writes for you: a top-level function becomes a static method on a class named FileNameKt, and a default argument becomes a synthetic $default bridge carrying a bitmask of which parameters were actually supplied (hence @JvmOverloads for Java). Finally, the scope functions: there are five, and they differ along just two axes — this versus it, and "hand back the receiver" versus "hand back the lambda's result". The layer-by-layer breakdown is below.
Topic map
- val, var, and the limits of immutability — why
valfreezes the reference rather than the object, what the backing field does, and how avalwith a custom getter returns a different value on every read. - if, when, and try as expressions — the value is the chosen branch's last expression, which is where the missing ternary operator and the exhaustiveness rule for
whenboth come from. - String templates —
$nameand${expr}, the compile-time lowering into concatenation, raw strings withtrimIndent(), and escaping a literal dollar sign. - The Unit type — a type with exactly one value versus
void, which is not a type at all, and why that matters for generics and lambdas. - The Any and Nothing hierarchy —
Anyas the root of the non-nullable types,Any?as the true top,Nothingas the bottom, and whatAnyactually declares. - Top-level functions — a function with no holder class, the generated
FileNameKtclass with static members, and renaming it with@JvmName. - Default and named arguments — the default as an expression evaluated on every call, the synthetic
$defaultbridge with its bitmask, and@JvmOverloadsfor Java. - Scope functions — the two axes of choice (
thisversusit, receiver versus lambda result), the one job of each of the five, and the cost of nesting.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Believing val makes the object immutable rather than just the reference | The object behind the reference is still mutable: a val holding a MutableList happily accepts add |
Caching the value of a val with a custom getter | The getter is recomputed on every read — two reads may legitimately differ |
Confusing val with a compile-time constant | That is const val: it is inlined at the use site and is allowed only for primitives and String |
| Looking for a ternary operator in Kotlin | There is none and never will be: if in expression position fills that role completely |
Forgetting that a when in expression position must be exhaustive | The code does not compile until you add else or cover every sealed/enum case |
Assuming when accepts constants only, like a switch | A subject-free when chains arbitrary conditions, in range checks, and is checks with a smart cast |
Thinking a string template is slower than + | The compiler lowers it to the very same concatenation — the cost is identical |
Forgetting ${} around an expression | Only the first name is interpolated: "$user.name" yields User(...) followed by a literal .name |
| Not escaping a literal dollar sign | The compiler reads $ as the start of a placeholder and fails on the unknown name |
Treating Unit as a keyword like void | Unit is a type with exactly one value; it fits as a generic argument, void does not |
Confusing Unit with Nothing | Unit has one value, Nothing has none: a function typed Nothing never returns at all |
Expecting wait, notify, and getClass as members of Any | Any declares only equals, hashCode, and toString |
Believing a variable typed Any can hold null | The top of the hierarchy is Any?, not Any; Any roots only the non-nullable types |
| Assuming no JVM class is generated for a top-level function | A class named FileNameKt with static members is generated — from Java it is UtilsKt.foo() |
| Carrying over Python's "the default is evaluated once at definition" | In Kotlin a default is an expression, evaluated afresh on every call that omits the argument |
Expecting Java to see the defaults without @JvmOverloads | Java sees the full signature plus a synthetic $default bridge — there are no overloads |
Reaching for apply when you need the lambda's computed value back | apply returns the receiver; the lambda's result is silently discarded |
| Judging nested scope functions by allocation cost | All five are inline, so no lambda is allocated: you pay in readability, not speed — it shadows it |
Interview relevance
The fundamentals are a first-pass filter. From the answer to "what is the difference between val and var" the interviewer learns in one sentence whether the candidate is thinking about the reference or about the object: "val means immutability" is a stop word; "val is a reference assigned exactly once, and the object behind it is still mutable" is a green light. The same logic runs through the whole topic: the questions are not about vocabulary but about the execution model — what the compiler physically puts into the bytecode when it meets a string template, a default in a signature, or a function declared outside any class.
Typical checks:
valversusvar, and the exact edge of thevalguarantee (the reference, not the object; the custom getter).- Why
ifandwhenare expressions, why there is no ternary operator, and when awhenmust be exhaustive. - What the compiler lowers a string template into, and how to insert a literal
$. Unitversusvoid, andUnitversusNothing.- Where
Any,Any?, andNothingsit, and whatAnyreally declares. - What a top-level function compiles to, and how to call it from Java.
- When a default argument is evaluated, and what
@JvmOverloadsgenerates. - The two axes for picking a scope function, and why nesting hurts.
Common wrong answer: "val is immutability, var is mutability." From there the candidate confidently derives the next claim: if val is immutable, then a val holding a MutableList will reject add, and two reads of a val must give the same result. Both derivations are false. val forbids reassigning the reference and says nothing whatsoever about the object: val items = mutableListOf<String>(); items.add("x") compiles and runs. And a val with a custom getter has no backing field at all and is recomputed on every access — which is exactly why such a property is not a smart-cast candidate either. The second classic failure is "a mutableListOf() default in the signature creates one shared list": that is Python's rule, not Kotlin's; in Kotlin the default is re-evaluated on every call that omits the argument.