Code snippet output questions
Practice reading Kotlin precisely, predicting deterministic output, and explaining the rule behind it.
All done - nice work.
Output questions are less about clever tricks than they first appear. A good interviewer is checking whether you can slow down, track values, and distinguish what Kotlin guarantees from what merely happened on one run.
A method that works under pressure
- Check that it compiles. Watch types, nullability, labels, and overloads.
- Mark evaluation order. Kotlin evaluates function arguments and receiver expressions in the order they appear.
- Track state on paper. Write down mutable variables, shared references,
and each side effect such as
printlnoradd. - Expand the convenient syntax. Ask what
?.,?:,let,copy, or a sequence pipeline actually does. - Separate dispatch rules. Members use virtual dispatch. Extensions are selected from the compile-time receiver type.
- Call out uncertainty. If threads or coroutines can interleave, give the guaranteed ordering only. Do not invent one exact output.
In an interview, give the output first. Then explain the two or three lines that decide it. If the snippet contains a bad production pattern, say so after you answer the question. That keeps the explanation focused without missing the engineering lesson.
The starred questions cover the patterns worth learning first: lazy sequences, shallow copies, extension dispatch, default arguments, null-safe evaluation, and expression-based control flow.
All snippets in this topic are intended to be deterministic on Kotlin/JVM. Time and thread scheduling are deliberately kept out unless the question explicitly states what is guaranteed.
Useful references
Frequently asked. Prioritize these in your first pass.
Start here
Core ideas you should be able to explain in plain language.
Reading Kotlin
What does return on the right side of the Elvis operator do?
fun lengthOrZero(text: String?): Int {
val value = text ?: return 0
println("measuring")
return value.length
}
println(lengthOrZero(null))
println(lengthOrZero("cat"))
Output:
0
measuring
3
return has the type Nothing, so Kotlin allows it on the right side of ?:.
For null, the function exits immediately and never prints "measuring". For
"cat", the Elvis branch is skipped and value is known to be non-null.
How to reason about it: treat ?: return ... as a guard clause. Split the
null and non-null paths before reading the remaining lines.
Collections and evaluation
In what order does a Kotlin Sequence run filter and map?
val result = sequenceOf(1, 2, 3, 4)
.filter {
println("filter $it")
it % 2 == 0
}
.map {
println("map $it")
it * 10
}
.take(1)
.toList()
println(result)
Output:
filter 1
filter 2
map 2
[20]
A Sequence is lazy. It takes one element through the pipeline at a time and
stops as soon as take(1) has one result. Element 1 fails the filter. Element
2 passes, gets mapped to 20, and the sequence has done enough work.
With a regular List, filter would inspect all four values before map
starts. That difference is the point of the question.
How to reason about it: find the terminal operation first. Here it is
toList(). Then move one element at a time through the intermediate operations.
What happens when associateBy produces the same key more than once?
data class User(val id: Int, val name: String)
val users = listOf(
User(1, "Asha"),
User(2, "Ben"),
User(1, "Cara"),
)
println(users.associateBy { it.id })
Output:
{1=User(id=1, name=Cara), 2=User(id=2, name=Ben)}
When more than one element produces the same key, the last value wins. The key
keeps its original insertion position in the returned map, which is why key 1
still appears before key 2.
If duplicates should be retained, use groupBy instead. It returns a list of
values for each key.
How to reason about it: build the map one input at a time. An existing key is updated rather than inserted again.
Objects and dispatch
Does data class copy() make a deep copy?
data class Team(
val name: String,
val members: MutableList<String>,
)
val first = Team("Android", mutableListOf("Maya"))
val second = first.copy(name = "Mobile")
second.members += "Noah"
println(first)
println(second)
println(first.members === second.members)
Output:
Team(name=Android, members=[Maya, Noah])
Team(name=Mobile, members=[Maya, Noah])
true
copy() is shallow. It creates a new Team, but properties that were not
replaced keep the same references. Both objects therefore point to one mutable
list.
In production code, prefer immutable collections in state models. If a true independent copy is required, copy the nested value as well:
val second = first.copy(
name = "Mobile",
members = first.members.toMutableList(),
)
How to reason about it: draw the two objects and their references. Do not assume that a new outer object means new nested objects.
Use it in practice
Common implementation choices, debugging, and trade-offs.
Reading Kotlin
Is the right side evaluated when a safe-call assignment is skipped?
data class Profile(var name: String)
data class User(val profile: Profile?)
fun loadName(): String {
println("loading")
return "Asha"
}
val user: User? = null
user?.profile?.name = loadName()
println("done")
Output:
done
loadName() is never called. If any receiver in a safe-call assignment is
null, Kotlin skips the assignment and does not evaluate its right-hand side.
This matters when the right side performs work. A safe-call chain can prevent that work entirely, not merely prevent the final property write.
How to reason about it: evaluate the receiver chain first. Only evaluate the right side if the chain reaches a non-null assignment target.
Objects and dispatch
Which Kotlin extension function is called when the runtime type is more specific?
open class Screen
class HomeScreen : Screen()
fun Screen.label() = "screen"
fun HomeScreen.label() = "home"
val screen: Screen = HomeScreen()
println(screen.label())
Output:
screen
Extensions do not add virtual members to a class. Kotlin chooses an extension
from the receiver’s compile-time type, which is Screen here. The runtime object
being a HomeScreen does not change that choice.
If label() were an overridden member function, normal virtual dispatch would
call the HomeScreen implementation.
How to reason about it: write the declared type next to the receiver. Use that type for extensions, and the runtime type for overridden members.
How do default arguments behave with overridden functions?
open class Base {
open fun greet(name: String = "base") = "Base: $name"
}
class Child : Base() {
override fun greet(name: String) = "Child: $name"
}
val value: Base = Child()
println(value.greet())
Output:
Child: base
Two rules work together. The default argument comes from the declaration chosen
at the call site, so Base.greet supplies "base". The function body uses
virtual dispatch, so Child.greet runs.
An override does not repeat default parameter values. Kotlin keeps those values on the base declaration.
How to reason about it: resolve omitted arguments using the variable’s compile-time type, then resolve the overridden body using the runtime type.
Control flow
What value does a Kotlin try expression return, and what does finally change?
fun parse(value: String): Int {
return try {
println("try")
value.toInt()
} catch (e: NumberFormatException) {
println("catch")
-1
} finally {
println("finally")
}
}
println(parse("x"))
Output:
try
catch
finally
-1
try is an expression in Kotlin. Because parsing throws, the catch block’s
last expression, -1, becomes the result. The finally block still runs before
the function returns, but its value is ignored.
A return or throw inside finally would override the earlier result. That
is legal, but it hides control flow and is best avoided.
How to reason about it: choose either the successful try path or a matching
catch path, record its result, run finally, and then return the recorded
result unless finally exits abruptly.
Discussion
Comments are powered by GitHub Discussions. Sign in with GitHub to join in.