Kotlin Interview Questions and Answers
Null safety, coroutines, extension functions and Java interop.
Whether you are preparing for entry-level Kotlin interview questions for freshers or senior software engineer interview questions addressing concurrency, scalability, and system architecture, this track provides peer-reviewed model answers with syntax walkthroughs, edge cases, and practical interview tips.
1 How does Kotlin handle null safety? Easy
Kotlin's type system distinguishes nullable from non-nullable types. String cannot hold null; String? can. This moves null checks to compile time and largely eliminates NullPointerException.
var a: String = "hi"
var b: String? = null
// a.length // compile error on b.length
println(b?.length) // safe call, prints null
println(b?.length ?: 0) // Elvis operator, default value
b?.let { println(it) } // runs only when non-null
val c = b!!.length // asserts non-null, throws if null
Tools include the safe call ?., the Elvis operator ?:, let/also, and the not-null assertion !!, which should be avoided because it reintroduces crashes. lateinit var is for non-null properties initialised after construction, and by lazy for deferred initialisation. Values coming from Java have platform types with unknown nullability, so guard or annotate them at the boundary.
2 What is the difference between val and var? Easy
val declares a read-only reference that cannot be reassigned after initialisation. var declares a mutable reference that can be reassigned.
val name = "Ada" // cannot reassign
var count = 0
count += 1 // ok
val list = mutableListOf(1)
list.add(2) // ok: the reference is fixed, contents are mutable
The restriction applies to the reference, not the object's internal state, which is why mutating a mutableList held by a val is legal. Prefer val by default: it makes code easier to reason about, is friendlier under concurrency and helps the compiler. Use var only when reassignment is genuinely needed. A val property can still have a custom getter that computes a fresh value on each access. For truly immutable objects, also make the underlying data immutable, since val alone does not prevent mutation through other references.
3 What is a data class and what does it generate? Medium
A data class automatically generates equals(), hashCode(), toString(), copy() and componentN() functions for destructuring, based on the properties declared in its primary constructor.
data class User(val id: Int, val name: String)
val u = User(1, "Ada")
val renamed = u.copy(name = "Grace")
val (id, name) = renamed // destructuring
println(renamed) // User(id=1, name=Grace)
Requirements: the primary constructor must have at least one parameter, all parameters must be val or var, and the class cannot be abstract, open, sealed or inner. Only primary-constructor properties participate in the generated methods; properties declared in the body are ignored.
Use data classes for value-like holders, DTOs and results. For persistence entities, generated equals and hashCode based on a mutable id can cause subtle bugs in sets and maps, so some teams use regular classes there.
4 What are Kotlin coroutines and when should you use them? Medium
Coroutines are Kotlin's solution for asynchronous and concurrent code. They are lightweight, suspendable computations: a suspend function can pause without blocking a thread, and the compiler turns it into a state machine.
suspend fun loadUser(id: Int): User = withContext(Dispatchers.IO) {
api.fetch(id) // runs on the IO dispatcher
}
fun main() = runBlocking {
val user = loadUser(1)
println(user)
}
Launching: launch for fire-and-forget work within a scope, async when you need a result, and coroutineScope or supervisorScope to structure failure. Dispatchers choose threads: Main for UI, IO for blocking calls, Default for CPU work. Because coroutines are cheap, you can start thousands of them.
Structured concurrency ties children to a parent scope, so they are cancelled and awaited together. Use coroutines for I/O and UI work, and as the modern replacement for callbacks and nested threads.
5 What are extension functions and what are their limitations? Medium
An extension function adds a method to a class without modifying or inheriting it. Resolution is static.
fun String.toTitleCase(): String =
split(" ").joinToString(" ") { word ->
word.replaceFirstChar { it.uppercase() }
}
"hello world".toTitleCase() // "Hello World"
Extensions are syntactic sugar for a static function whose first parameter is the receiver. They do not truly add members, which has consequences:
- They cannot access
privateorprotectedmembers. - They are not virtual: dispatch is based on the declared static type, not the runtime type, so an extension can be shadowed unexpectedly.
- A real member with the same signature always wins.
- Extension properties cannot store state because there is no backing field.
Common uses are utility functions, Android view helpers and DSL builders like buildString and apply. Keep them in dedicated files so they do not become a dumping ground.
6 When would you use a sealed class instead of an enum? Medium
An enum class defines a fixed set of constants, each a single instance. It can have properties and methods, but every constant shares the same shape.
A sealed class or sealed interface restricts subclasses to the same package and module, giving a closed hierarchy where each subclass can hold different data.
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val code: Int) : Result()
object Loading : Result()
}
fun render(r: Result) = when (r) {
is Result.Success -> r.data
is Result.Error -> "error ${r.code}"
Result.Loading -> "loading"
}
Because the hierarchy is closed, when is exhaustive and the compiler warns about a missing branch when used as an expression. Use enums for simple named values, and sealed types for state machines, API responses and algebraic data types where each case carries different data.
7 How do higher-order functions and inline functions work? Medium
A higher-order function takes a function as a parameter or returns one. Functions are first-class values with types like (Int) -> Int.
fun <T> List<T>.myFilter(predicate: (T) -> Boolean): List<T> {
val out = mutableListOf<T>()
for (item in this) if (predicate(item)) out.add(item)
return out
}
val evens = (1..10).myFilter { it % 2 == 0 }
Lambdas passed to such functions are objects, so each call allocates and adds an indirect call. Marking the function inline substitutes its body at the call site, eliminating the lambda object and enabling non-local returns, where a return inside the lambda returns from the enclosing function.
inline fun measure(block: () -> Unit) {
val start = System.nanoTime()
block()
println(System.nanoTime() - start)
}
Standard functions like let, run and apply are inline. Avoid inlining large functions or functions that store their lambda parameters.
8 Compare the scope functions let, run, with, apply and also. Medium
All five run a block with an object in context. They differ in how the object is referenced (it or this) and what they return (the object or the lambda result).
let:it, returns the lambda result. Used for null checks and transformations.run:this, returns the result. Configuration plus computation.with:this, returns the result. Grouping several calls on one object.apply:this, returns the object. Object configuration.also:it, returns the object. Side effects such as logging.
val user = User().apply {
name = "Ada"
age = 36
}
val upper = user.let { it.name.uppercase() }
Choose based on chaining needs rather than habit: use apply at the end of a builder chain, also for side effects, and let when you need the result. Avoid nesting them so deeply that this becomes ambiguous, and keep blocks short.
9 How does structured concurrency and cancellation work with coroutines? Hard
A coroutine is cooperative: cancellation sets a flag that the coroutine must observe. Suspend functions from kotlinx.coroutines check automatically and throw CancellationException, but a tight loop that never suspends is not cancellable unless you check.
val job = launch {
repeat(1000) { i ->
ensureActive() // or check isActive
process(i)
}
}
job.cancelAndJoin()
Structured concurrency means a scope cannot complete until its children do. coroutineScope waits for children and cancels siblings when one fails; supervisorScope isolates failures so one child does not cancel the others. Cancellation propagates from parent to child.
Never swallow CancellationException. If cleanup must suspend during cancellation, wrap it in withContext(NonCancellable). This model prevents leaked background work and is why launching coroutines in a scope you control matters.
10 Explain declaration-site variance with in and out. Hard
Variance describes how generic types relate when their type arguments are related. out T (covariant) means Producer<String> can be used where Producer<Any> is expected. in T (contravariant) means Consumer<Any> can be used where Consumer<String> is expected.
interface Producer<out T> { fun produce(): T } // only returns T
interface Consumer<in T> { fun consume(t: T) } // only accepts T
fun feed(c: Consumer<Any>) { c.consume("hi") }
val stringConsumer: Consumer<String> = ...
feed(stringConsumer) // safe: a String consumer handles Any
Kotlin uses declaration-site variance with out and in; Java uses use-site wildcards (? extends, ? super). The safety rule: an out parameter may only appear in return positions, and an in parameter only in parameter positions. Violating this is a compile error, which prevents unsound assignments.
Kotlin also supports use-site projections for individual calls and star projections like List<*> when the type argument is unknown. Understanding variance explains why List is covariant but MutableList is invariant.
Frequently Asked Questions About Kotlin Interviews
What do hiring managers evaluate in Kotlin technical rounds?
Technical interviewers look for foundational fluency, idiomatic syntax, clarity when communicating complex logic, and awareness of performance trade-offs (e.g. memory footprint, render performance, and network latency) in production environments.
What are the best interview tips for practicing Kotlin questions?
Use active recall: summarize each answer in your own words before revealing the model solution. Focus on explaining why a certain approach is chosen rather than just memorizing code syntax.