Kotlin Fundamentals
The everyday syntax layer — val versus var, expression-oriented if and when, string templates, Unit, the Any hierarchy, default and named arguments, top-level functions, and the scope functions let/run/with/apply/also.
12 questions
JuniorTheoryVery commonWhy are if and when expressions in Kotlin rather than statements?
Why are if and when expressions in Kotlin rather than statements?
They produce a value, so you can assign one directly: val max = if (a > b) a else b. The value is the last expression of the chosen branch. That is why Kotlin has no ternary operator, and why a when used as an expression must cover every case.
Common mistakes
- ✗Looking for a ternary operator instead of using
ifas an expression - ✗Forgetting that a
whenused as an expression must be exhaustive - ✗Thinking the value is the whole branch rather than its last expression
Follow-up questions
- →When is an
elsebranch required in awhen, and when may it be omitted? - →What is the type of an
ifwhose two branches return different types?
JuniorTheoryVery commonWhat is the difference between val and var when declaring a variable?
What is the difference between val and var when declaring a variable?
var declares a variable you can reassign; val declares one assigned exactly once. val freezes the reference, not the object behind it — a val holding a MutableList still accepts add. Prefer val by default; use var only when needed.
Common mistakes
- ✗Believing
valmakes the referenced object immutable, not just the reference - ✗Treating
valas a style hint the compiler does not enforce - ✗Assuming
valmeans a compile-time constant — that isconst val
Follow-up questions
- →Why can a
valproperty with a custom getter return a different value on each read? - →What does
const valadd on top ofval, and where may it be declared?
JuniorTheoryCommonWhat do default arguments and named arguments give you over overloading?
What do default arguments and named arguments give you over overloading?
A default value in the signature lets one function replace a telescoping chain of overloads. Named arguments then let the caller skip the middle defaults and pass only what matters — send(msg, retries = 3) — and make a bare boolean flag readable.
Common mistakes
- ✗Assuming a default value is evaluated once and shared between calls
- ✗Expecting Java callers to see the overloads without
@JvmOverloads - ✗Thinking a default must be a compile-time constant rather than any expression
Follow-up questions
- →What does the interop annotation
@JvmOverloadsgenerate for a defaulted function? - →Why does inserting a new parameter in the middle of a signature break existing callers?
JuniorTheoryCommonWhat is Unit, and how does it differ from Java's void?
What is Unit, and how does it differ from Java's void?
Unit is a real type with exactly one value, the Unit object, returned by a function with no useful result. Java's void is not a type and has no value, so it cannot be a type argument; Unit can — a lambda returning nothing is () -> Unit.
Common mistakes
- ✗Treating
Unitas a keyword likevoidrather than a type with a value - ✗Confusing
UnitwithNothing, the type that has no values at all - ✗Writing
Voidin a lambda type whereUnitis what Kotlin expects
Follow-up questions
- →How does
Unitdiffer fromNothing, the type with no values? - →What does a Kotlin function returning
Unitlook like from Java?
MiddleTheoryCommonHow do the scope functions let, run, with, apply and also differ?
How do the scope functions let, run, with, apply and also differ?
They differ on two axes. The context object is either the receiver this (run, with, apply) or the argument it (let, also). The result is either the lambda's value (let, run, with) or the context object itself (apply, also).
Common mistakes
- ✗Reaching for
applywhen the lambda's computed value is what you need back - ✗Assuming every scope function exposes the object the same way
- ✗Nesting scope functions until
itandthisshadow each other
Follow-up questions
- →Which scope function fits a null-safe transformation of a nullable value, and why?
- →Why does
withtake its context object as an argument whilerundoes not?
JuniorTheoryOccasionalWhat do string templates give you over concatenation or String.format?
What do string templates give you over concatenation or String.format?
A template interpolates values into the literal: $name for a bare name, ${age + 1} for any expression. It lowers to a StringBuilder chain, so it costs the same as manual concatenation, and unlike String.format no format string can drift.
Common mistakes
- ✗Thinking a template is slower than
+because it looks like interpolation - ✗Forgetting
${}around an expression and interpolating only the first name - ✗Not escaping a literal dollar sign, which the compiler reads as a placeholder
Follow-up questions
- →How do you put a literal
$character into a templated string? - →What does a raw string literal add on top of an ordinary templated string?
JuniorTheoryOccasionalWhy does Kotlin allow top-level functions instead of a static utility class?
Why does Kotlin allow top-level functions instead of a static utility class?
A function can live directly in a file, so a helper needs no class that exists only to hold it. The compiler still emits one: Utils.kt becomes a class UtilsKt with static members, so Java calls UtilsKt.foo() while Kotlin imports the function.
Common mistakes
- ✗Assuming no JVM class is generated, so Java cannot call the function
- ✗Wrapping helpers in an
objectout of habit when a file-level function suffices - ✗Expecting Java to see the function as a member of a class named after the package
Follow-up questions
- →How do you change the name of the generated JVM class for a Kotlin file?
- →When is an extension function a better fit than a top-level function?
MiddleTheoryOccasionalWhere do Any and Any? sit in Kotlin's type hierarchy versus Java's Object?
Where do Any and Any? sit in Kotlin's type hierarchy versus Java's Object?
Any roots all non-nullable types; Any? is the true top, admitting null. Any declares only equals, hashCode and toString — no wait/notify like Object. Unit is an ordinary type under Any, so returning nothing returns a value.
Common mistakes
- ✗Expecting
Anyto exposewait,notifyand the rest ofObject - ✗Believing a variable typed
Anycan holdnullwithout the? - ✗Placing
Unitoutside the hierarchy instead of underAny
Follow-up questions
- →How do you call
waitornotifyon a value whose static type isAny? - →Which type sits at the very bottom of the hierarchy, below every other type?
MiddleCodeOccasionalPredict the output: a default argument that allocates a fresh list
Predict the output: a default argument that allocates a fresh list
It prints [a], then [b], then [x, c]. A default value is an expression evaluated afresh on every call that omits the argument, so each defaulted call builds its own list. Passing shared explicitly appends to that list instead.
Common mistakes
- ✗Carrying over Python's habit of a default argument evaluated once at definition
- ✗Assuming an argument passed explicitly is copied rather than shared
- ✗Believing a default value must be a constant, not an arbitrary expression
Follow-up questions
- →May a default value refer to an earlier parameter of the same function?
- →What happens to the defaults when the function is called from Java?
SeniorTheoryOccasionalWhat does val actually guarantee, and where does that guarantee stop?
What does val actually guarantee, and where does that guarantee stop?
val guarantees only that the reference is assigned once. The object behind it can still mutate: a val holding a MutableList accepts add. And a val property with a custom getter is recomputed on every read, so two reads may differ.
Common mistakes
- ✗Handing out a
valMutableListand calling the API immutable - ✗Caching the result of a
valwith a custom getter, assuming it never changes - ✗Equating
valwith Java'sfinalon the object rather than on the reference
Follow-up questions
- →How would you expose a collection field so that callers genuinely cannot modify it?
- →Why is a
valwith a custom getter not a candidate for smart casting?
SeniorTheoryOccasionalHow does Kotlin's when compare with Java's pattern-matching switch?
How does Kotlin's when compare with Java's pattern-matching switch?
Both match on type and value and both are expressions. But when needs no subject — a bare when { a > b -> … } chains arbitrary conditions — and matches ranges with in. switch needs a selector; in when, a type check smart-casts the subject.
Common mistakes
- ✗Assuming
whenaccepts constants only, like a classicswitch - ✗Missing that a type check in
whensmart-casts the subject inside the branch - ✗Forgetting that a subject-free
whenreplaces an if/else-if chain entirely
Follow-up questions
- →When does the compiler let you omit
elsefrom awhenover a sealed hierarchy? - →What does a subject-free
whenbuy you over a chain ofif/else if?
SeniorDesignRareYou are reviewing a pull request in a Kotlin backend service. The author has wrapped almost every statement in a scope function. A repository lookup reads repo.find(id)?.let { it.also { log(it) } }?.run { toDto() }. A configuration builder nests apply inside apply three levels deep, so that this inside the innermost lambda is ambiguous to a reader. A validation helper uses also purely to mutate a variable captured from the enclosing function. The team's stated goal is code a new hire can read on the first pass. Give your review feedback: say which of these uses are justified and which are not, explain what the reader actually loses when it and this shadow one another in nested scope functions, and state the concrete rules you would put in the team's style guide so that let, run, apply and also each keep one clear job. Do not simply answer 'use them less' — name the criterion you would apply at each call site.
You are reviewing a pull request in a Kotlin backend service. The author has wrapped almost every statement in a scope function. A repository lookup reads repo.find(id)?.let { it.also { log(it) } }?.run { toDto() }. A configuration builder nests apply inside apply three levels deep, so that this inside the innermost lambda is ambiguous to a reader. A validation helper uses also purely to mutate a variable captured from the enclosing function. The team's stated goal is code a new hire can read on the first pass. Give your review feedback: say which of these uses are justified and which are not, explain what the reader actually loses when it and this shadow one another in nested scope functions, and state the concrete rules you would put in the team's style guide so that let, run, apply and also each keep one clear job. Do not simply answer 'use them less' — name the criterion you would apply at each call site.
Pick by what each hands back: let transforms, run computes a value, apply configures an object you return, also runs a side effect. Nesting shadows it/this — a readability cost, not speed. One scope function per expression.
Common mistakes
- ✗Judging scope-function abuse by allocation cost instead of by readability
- ✗Picking a scope function by receiver nullability rather than by what it returns
- ✗Using
alsoas a general-purpose block that mutates variables outside it
Follow-up questions
- →How would you rewrite the repository chain so that each step has one obvious purpose?
- →When does naming the lambda parameter beat relying on the implicit
it?