← All topics
Kotlin

Code snippet output questions

Practice reading Kotlin precisely, predicting deterministic output, and explaining the rule behind it.

8 questions ★ 6 high priority 4 junior4 mid
Code snippet output questions progress 0 of 8 completed (0%)

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

  1. Check that it compiles. Watch types, nullability, labels, and overloads.
  2. Mark evaluation order. Kotlin evaluates function arguments and receiver expressions in the order they appear.
  3. Track state on paper. Write down mutable variables, shared references, and each side effect such as println or add.
  4. Expand the convenient syntax. Ask what ?., ?:, let, copy, or a sequence pipeline actually does.
  5. Separate dispatch rules. Members use virtual dispatch. Extensions are selected from the compile-time receiver type.
  6. 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?
Junior #kotlin#output-based#null-safety#elvis
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?
Junior #kotlin#output-based#sequences#collections
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?
Junior #kotlin#output-based#collections#maps
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?
Junior #kotlin#output-based#data-class#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?
Mid #kotlin#output-based#null-safety#evaluation
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?
Mid #kotlin#output-based#extensions#dispatch
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?
Mid #kotlin#output-based#default-arguments#inheritance
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?
Mid #kotlin#output-based#exceptions#expressions
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.