Kotlin Interview Questions
Commonly asked Kotlin interview questions on null safety, data classes, extension functions, and coroutines.
A curated set of Kotlin interview questions, ordered roughly from fundamentals to more advanced — the kind you'll actually be asked in real screens and on-sites.
Null safety
Q: How does Kotlin's null safety model actually work?
Kotlin's type system distinguishes nullable types (String?) from non-nullable types (String) at compile time — a non-nullable type can never legally be assigned or passed null, so the compiler statically rejects any code path where that could happen. To work with a value that might genuinely be absent, you declare it as nullable and the compiler then requires you to handle that case explicitly — via a safe call (?.), the Elvis operator (?:), an explicit null check (which lets the compiler "smart-cast" the value to non-null inside that branch), or the !! escape hatch, which throws immediately if the value actually is null.
Q: When, if ever, should you use !!?
Sparingly — mainly at the boundary with Java code (which has no compile-time nullability information at all) or in cases where you can prove a value can never be null but the compiler's flow analysis can't see that. Reaching for !! routinely just to silence the compiler defeats the entire purpose of Kotlin's null-safety system and reintroduces the same NullPointerException risk Kotlin was designed to eliminate.
OOP
Q: What's the difference between a data class and a regular class?
A data class automatically generates equals()/hashCode() (based on the properties in its primary constructor, comparing by content), a readable toString(), and a copy() method for producing a modified shallow copy — all without writing any of it by hand. A regular class gets none of that for free; == on a plain class compares references unless you write your own equals().
Q: What is an extension function, and how is it resolved?
It's a function that appears to add a new method to an existing type — even one you don't own the source of — without subclassing or modifying it, written as fun ReceiverType.functionName(). It's resolved statically at compile time based on the declared type of the expression it's called on (not the actual runtime type), which is an important distinction from true polymorphic method overriding.
Q: Why are Kotlin classes final by default, unlike Java's classes?
It's a deliberate design choice to force an explicit decision about whether a class is meant to be extended — you must mark a class (and the specific members you want overridable) as open. This avoids the common real-world problem of a class being subclassed in ways its original author never intended or tested for, simply because nothing stopped it.
Concurrency
Q: How do Kotlin coroutines differ from plain OS threads?
Coroutines are a much lighter-weight abstraction managed by the Kotlin runtime rather than the OS — thousands can run concurrently on a small pool of just a few real threads, because a suspended coroutine simply steps aside (via suspend/delay) instead of blocking the thread it's running on, letting that thread do other useful work in the meantime. Threads, by contrast, are expensive to create and always block for their full wait duration.
Q: What does "structured concurrency" mean in the context of Kotlin coroutines?
It means every coroutine is launched inside a CoroutineScope, and that scope won't complete until every coroutine launched within it has finished — child coroutines can't outlive their parent scope, and an exception in one child properly propagates and cancels its siblings instead of failing silently in the background. This gives coroutine-based concurrent code the same predictable lifetime and error-handling guarantees as ordinary sequential code, which raw threads don't provide on their own.
Null safety in depth
Q: What's the difference between let and also, given both refer to the receiver as it?
let returns the result of its block, making it the right choice when you want to transform a value or compute something new from it — often combined with a safe call (nickname?.let { ... }) to run code only when the receiver isn't null. also always returns the original object itself regardless of what the block computes, making it the right choice for a side effect (logging, an extra mutation, a validation check) performed midway through a chain, where the original object still needs to flow on to the next step unchanged.
Q: What is a Kotlin platform type, and why does it exist?
It's the type Kotlin assigns to a value coming from Java code (notated internally as Type!), because Java's type system carries no nullability information at all — Kotlin genuinely can't know from a Java method's signature whether its result can be null. Rather than blocking Java interop entirely or forcing a null check around every single Java call, Kotlin lets a platform type be treated as either nullable or non-nullable at the call site, trusting the developer to know the actual Java contract — which means a NullPointerException remains possible specifically at that interop boundary, unlike with ordinary Kotlin-declared non-nullable types.
Kotlin for Android
Q: Why is a sealed class typically preferred over several nullable fields and boolean flags for modeling a screen's UI state?
A sealed class makes invalid combinations structurally impossible — a screen can't be simultaneously "loading" and "showing an error," because those are distinct subclasses rather than independent flags that could accidentally both be set. It also lets the compiler enforce exhaustiveness on any when that consumes the state, so adding a new state later immediately flags every place in the codebase that needs to be updated to handle it, instead of silently leaving old assumptions about a smaller set of combinations in place.
Q: Why do Android list adapters (like a RecyclerView adapter) benefit specifically from data classes as item models?
A data class's generated equals()/hashCode() compare by content rather than by reference, which is exactly what's needed when diffing an old list against a new one to determine which rows actually changed, were added, or were removed. Comparing by reference (a plain class's default) would treat every item as "different" on every update regardless of its actual content, defeating the point of an efficient, minimal-redraw diff.
Testing
Q: What does assertFailsWith<ExceptionType> { ... } verify, beyond just "the block throws something"?
It runs the block and asserts that it throws an exception of exactly the specified type, failing the test if nothing is thrown or if a different, unrelated exception type is thrown instead — and it returns the caught exception so further assertions can be chained against its message or properties. This is Kotlin's idiomatic equivalent of PHPUnit's expectException or Java JUnit's assertThrows, giving a stronger guarantee than simply catching any Throwable.
Q: Why might a Kotlin test reach for a small hand-written fake implementing an interface instead of a mocking library? Kotlin's concise class syntax makes writing a simple fake — implementing an interface with a few tracked properties, like a call counter — barely more code than configuring an equivalent mock through a mocking library's API, while staying fully readable as ordinary Kotlin with no additional DSL to learn. A fake also behaves like real code with no hidden proxying, which tends to make failures easier to reason about; mocking libraries like MockK still earn their place for more elaborate verification needs, but a fake is often the simpler default for straightforward dependencies.