Top 60 Kotlin Interview Questions (2026)

The 60 Kotlin questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.

60 questions with answers

What Is Kotlin?

Key Takeaways

  • Kotlin is a statically typed, JVM language from JetBrains with null safety built into the type system.
  • It's fully interoperable with Java, so teams adopt it file by file inside existing Java codebases.
  • Interviews test null safety, coroutines, and how Kotlin features compile down to the JVM, not just syntax.
  • This page is a question bank: work your tier first, read one tier up, and say answers out loud because delivery gets evaluated too.

Kotlin is a statically typed programming language from JetBrains that runs on the Java Virtual Machine and compiles to JVM bytecode, JavaScript, and native binaries. Google named it the preferred language for Android development in 2019, and it's built around one headline idea: nullability lives in the type system, so a whole class of NullPointerException bugs gets caught at compile time. It's fully interoperable with Java, which is why teams migrate one file at a time instead of rewriting. In interviews, Kotlin questions probe null safety, coroutines, and how the sugar (data classes, extension functions, sealed classes) compiles down to plain JVM constructs, not memorized trivia. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're building fundamentals, the official Kotlin documentation from JetBrains is the canonical path; increasingly the first Kotlin round runs as a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.

60Questions with answers on this page
3Difficulty tiers: freshers, intermediate, experienced
24Runnable Kotlin snippets you can practice from
45-60 minTypical length of a Kotlin technical round

Watch: Kotlin Course - Tutorial for Beginners

Video: Kotlin Course - Tutorial for Beginners (freeCodeCamp.org, YouTube)

Test yourself and earn a certificate

10 quick questions. Score 70%+ to download your Kotlin certificate.

Jump to quiz

All Questions on This Page

60 questions
Kotlin Interview Questions for Freshers
  1. 1. What is Kotlin and what makes it popular?
  2. 2. What is the difference between val and var?
  3. 3. How does Kotlin handle null safety?
  4. 4. What do the safe call operator ?. and the Elvis operator ?: do?
  5. 5. What does the not-null assertion operator !! do, and when should you avoid it?
  6. 6. What is a data class and what does it generate?
  7. 7. What is the difference between == and === in Kotlin?
  8. 8. How does the when expression work?
  9. 9. Why is if an expression in Kotlin, and what does that replace?
  10. 10. Does Kotlin need explicit types, and how does type inference work?
  11. 11. How do you declare a nullable type, and how is it different from a non-null type?
  12. 12. What are string templates in Kotlin?
  13. 13. How do you declare a function in Kotlin, and what are default and named arguments?
  14. 14. What is the difference between List and MutableList?
  15. 15. How do loops and ranges work in Kotlin?
  16. 16. What is a companion object and why does Kotlin have one?
  17. 17. What is the difference between List<String?>, List<String>?, and List<String?>?
  18. 18. What are Any, Unit, and Nothing?
  19. 19. What is a smart cast?
  20. 20. What is the difference between as and as? for casting?
  21. 21. What is a single-expression function?
  22. 22. What are Kotlin's visibility modifiers, and how do they differ from Java's?
  23. 23. What is the difference between a primary and a secondary constructor?
  24. 24. How can the Elvis operator return or throw early?
  25. 25. Does Kotlin have checked exceptions?
Kotlin Intermediate Interview Questions
  1. 26. What are extension functions and how are they resolved?
  2. 27. What are higher-order functions and lambdas in Kotlin?
  3. 28. What are the scope functions let, run, with, apply, and also?
  4. 29. What are coroutines and how do they differ from threads?
  5. 30. What is a suspend function and where can you call it?
  6. 31. What is the difference between launch and async?
  7. 32. What is structured concurrency in coroutines?
  8. 33. What is a sealed class and when do you use one?
  9. 34. What is the difference between a sealed class and an enum?
  10. 35. What does the object keyword do?
  11. 36. Which collection operations should every Kotlin developer know?
  12. 37. What is the difference between a Sequence and a List for chained operations?
  13. 38. How do generics work in Kotlin, and what are in and out?
  14. 39. What are reified type parameters?
  15. 40. What are inline functions and why do they exist?
  16. 41. What is the difference between lateinit and lazy?
  17. 42. What is delegation with the by keyword?
  18. 43. How does destructuring work in Kotlin?
  19. 44. What are infix functions and operator overloading?
  20. 45. How does try/catch/finally work in Kotlin, and how is it an expression?
Kotlin Interview Questions for Experienced Developers
  1. 46. What is a CoroutineContext, and what do dispatchers do?
  2. 47. How does coroutine cancellation work, and how do you make code cancellable?
  3. 48. What is a Flow, and how does it differ from a suspend function and from a Channel?
  4. 49. How does exception handling work in coroutines?
  5. 50. What do noinline and crossinline mean on inline function parameters?
  6. 51. What is a value class (inline class), and why use one?
  7. 52. How does Kotlin interoperate with Java, and where does it get sharp?
  8. 53. How is Kotlin null safety enforced given the JVM has no such concept?
  9. 54. How do you write a custom property delegate?
  10. 55. What is the contract between equals and hashCode, and how do data classes handle it?
  11. 56. What is Kotlin Multiplatform and how does expect/actual work?
  12. 57. What does the tailrec modifier do?
  13. 58. How does Kotlin support building type-safe DSLs?
  14. 59. Why do smart casts fail on some properties, and what are contracts?
  15. 60. What performance costs are hidden in idiomatic Kotlin, and how do you address them?

Kotlin Interview Questions for Freshers

Freshers25 questions

The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.

Q1. What is Kotlin and what makes it popular?

Kotlin is a statically typed language from JetBrains that runs on the JVM and interoperates fully with Java. It's known for concise syntax and null safety built into the type system.

Its popularity took off when Google made it the preferred language for Android in 2019. Teams also reach for it on the backend because it removes Java's boilerplate while reusing the entire Java ecosystem, so migration happens one file at a time.

Key point: A one-line definition plus two concrete draws (null safety, Java interop) beats a memorized feature list. Interviewers open with this to hear how you organize an answer.

Watch a deeper explanation

Video: Kotlin Crash Course (Traversy Media, YouTube)

Q2. What is the difference between val and var?

val declares a read-only reference: once assigned, it can't be reassigned. var declares a reassignable reference. Prefer val by default and reach for var only when you actually mutate.

One trap: val is not deep immutability. A val pointing at a mutable list still lets you change the list's contents; it only stops you from pointing the name at a different list.

kotlin
val name = "Asha"
// name = "Ben"        // error: val cannot be reassigned

var count = 0
count = 1              // fine

val items = mutableListOf(1, 2)
items.add(3)           // allowed: the list is mutable, the reference is not

Key point: Saying 'val is a read-only reference, not deep immutability' is exactly the nuance the key signal is.

Q3. How does Kotlin handle null safety?

Kotlin splits every type into nullable and non-nullable at compile time. String can never hold null; String? can. The compiler forces you to handle the null case before you use a nullable value, so most NullPointerExceptions get caught before the code runs.

The tools are the safe call ?., the Elvis operator ?:, and the not-null assertion !!. Null safety in the type system is Kotlin's headline feature.

kotlin
var a: String = "hi"
// a = null            // compile error

var b: String? = "hi"
b = null               // fine
println(b?.length)     // safe call: prints null, no crash

Key point: The compile-time part. 'Nullability is in the type system' is the sentence that separates a real answer from a memorized one.

Q4. What do the safe call operator ?. and the Elvis operator ?: do?

?. calls a member only if the receiver is non-null, otherwise the whole expression is null. ?: returns its left side when that side is non-null, otherwise the right side, so it supplies a default.

Chaining them is the idiomatic null-handling pattern: reach into a nullable chain safely, then fall back to a default.

kotlin
val length: Int = name?.length ?: 0
// if name is null, length becomes 0

val city = user?.address?.city ?: "unknown"
// safe navigation through a nullable chain with a default

Key point: The follow-up is often 'what does !! do?'. Have the 'asserts non-null, throws if wrong, use sparingly' answer ready.

Q5. What does the not-null assertion operator !! do, and when should you avoid it?

!! converts a nullable type to its non-null counterpart and throws a NullPointerException if the value is actually null. It tells the compiler 'trust me, this isn't null.'

Avoid it whenever a safe call plus Elvis, or a null check, would work. Reaching for !! is often a sign the nullability wasn't modeled properly. It brings back the exact crash Kotlin is designed to prevent.

kotlin
val name: String? = null
// val length = name!!.length   // throws NullPointerException

// prefer:
val length = name?.length ?: 0

Q6. What is a data class and what does it generate?

A data class holds data and auto-generates equals(), hashCode(), toString(), componentN() for destructuring, and copy() from the properties in its primary constructor. It replaces a page of Java boilerplate with one line.

copy() is the everyday tool: make a modified duplicate while keeping the original untouched, which fits immutable-style code.

kotlin
data class User(val name: String, val age: Int)

val a = User("Asha", 30)
val b = a.copy(age = 31)      // new object, name kept
println(a == b)              // false, structural equality
val (name, age) = a          // destructuring via componentN()

Key point: Mention that equals compares by value, not reference. That structural-equality point is the usual follow-up.

Q7. What is the difference between == and === in Kotlin?

== checks structural equality and calls equals(); === checks referential equality, whether two names point to the same object in memory. This is the reverse of Java, where == is referential.

For data classes, == compares field by field. Use === only when you specifically care about object identity.

kotlin
val a = User("Asha", 30)
val b = User("Asha", 30)
println(a == b)    // true  (structural, same values)
println(a === b)   // false (different objects)

Key point: Contrasting it with Java's == out loud shows you understand both languages, which matters on JVM teams.

Q8. How does the when expression work?

when replaces the switch statement and is an expression, so it returns a value. It matches literals, ranges (in 1..10), types (is String), and arbitrary boolean conditions, with no fall-through and no break.

When used as an expression with a return value, it must be exhaustive, which the compiler enforces for sealed types and enums.

kotlin
fun describe(x: Any): String = when (x) {
    0 -> "zero"
    in 1..9 -> "single digit"
    is String -> "a string of length ${x.length}"
    else -> "something else"
}

Q9. Why is if an expression in Kotlin, and what does that replace?

if returns a value in Kotlin, so you assign its result directly. This is why Kotlin has no ternary operator (a ? b : c); if/else fills that role and reads better.

The same expression-orientation applies to when and try, which keeps code assignment-based rather than statement-heavy.

kotlin
val max = if (a > b) a else b
// no ternary needed; if is an expression

Q10. Does Kotlin need explicit types, and how does type inference work?

Kotlin is statically typed, but the compiler infers types from the right-hand side, so you often skip annotations. val count = 5 is inferred as Int; val name = "Asha" as String.

Types are still checked at compile time; inference just removes the noise. Annotate explicitly for public APIs and where inference would pick a wider or surprising type.

kotlin
val count = 5           // inferred Int
val pi = 3.14           // inferred Double
val names = listOf("a") // inferred List<String>
val total: Long = 5     // explicit when you want a wider type

Q11. How do you declare a nullable type, and how is it different from a non-null type?

Add a question mark: String? can hold null, String cannot. The two are distinct types, and the compiler won't let you assign null to a non-null type or use a nullable one without a null check.

This split is what moves null bugs from runtime to compile time. Default to non-null and add ? only where absence is a real, expected state.

kotlin
val required: String = "value"   // never null
val optional: String? = null     // may be null

fun greet(name: String?) {
    println("Hi ${name ?: "there"}")
}

Q12. What are string templates in Kotlin?

String templates embed variables and expressions directly in a string with $name for a variable and ${expression} for anything more complex. No concatenation, no format placeholders.

They're evaluated at runtime and are the everyday way to build strings, similar to Python f-strings or JavaScript template literals.

kotlin
val name = "Asha"
val items = listOf(1, 2, 3)
println("Hi $name, you have ${items.size} items")

Q13. How do you declare a function in Kotlin, and what are default and named arguments?

Functions use the fun keyword with parameter types after each name and the return type after the parameter list. Single-expression functions drop the braces and use =.

Default arguments give parameters fallback values, and named arguments let callers pass them in any order and skip the ones with defaults, which removes the need for many overloads.

kotlin
fun greet(name: String, greeting: String = "Hello"): String =
    "$greeting, $name"

greet("Asha")                       // "Hello, Asha"
greet("Ben", greeting = "Hi")       // "Hi, Ben"

Q14. What is the difference between List and MutableList?

List is a read-only interface: you can read and iterate but there's no add or remove. MutableList extends it with mutating operations. listOf() returns a List, mutableListOf() returns a MutableList.

Read-only is not the same as immutable. A List reference can point at a list that something else still mutates. It's about the interface you expose, not a hard guarantee.

kotlin
val fixed = listOf(1, 2, 3)          // List, no add()
val growing = mutableListOf(1, 2)    // MutableList
growing.add(3)                       // allowed

Key point: The 'read-only interface, not deep immutability' distinction is the point of the question. Say it explicitly.

Q15. How do loops and ranges work in Kotlin?

Kotlin has no C-style for loop. You iterate over ranges and collections: for (i in 1..5), for (item in list). Ranges use .. (inclusive), until (exclusive end), downTo, and step.

while and do-while exist as usual. For index and value together, use withIndex() rather than a manual counter.

kotlin
for (i in 1..3) print(i)          // 123
for (i in 0 until 3) print(i)     // 012
for (i in 10 downTo 1 step 3) print("$i ")  // 10 7 4 1
for ((idx, v) in listOf("a", "b").withIndex()) println("$idx=$v")

Q16. What is a companion object and why does Kotlin have one?

Kotlin has no static keyword. A companion object is a single object tied to a class that holds members you'd call statically in Java: factory methods, constants, and class-level state. You access them through the class name.

Under the hood it's a real object instance, so companion members can implement interfaces and be extended, which plain Java statics can't.

kotlin
class User private constructor(val name: String) {
    companion object {
        fun create(name: String) = User(name)
    }
}

val u = User.create("Asha")   // called on the class, like a static factory

Q17. What is the difference between List<String?>, List<String>?, and List<String?>?

List<String?> is a non-null list that may contain null elements. List<String>? is a nullable list of non-null strings, so the whole list can be null. List<String?>? is both: a nullable list that may hold null elements.

Reading these precisely matters because the ? placement changes exactly what can be null.

TypeList itself null?Elements null?
List<String>NoNo
List<String?>NoYes
List<String>?YesNo
List<String?>?YesYes

Key point: This tests whether you actually read types carefully. Walk through each ? out loud rather than guessing.

Q18. What are Any, Unit, and Nothing?

Any is the root of the non-null type hierarchy, like Object in Java. Unit is the type of a function that returns no meaningful value; it has one instance and is usually omitted. Nothing is the type with no values, returned by functions that never return normally (they always throw or loop forever).

Nothing is what makes throw usable as an expression and lets the compiler know code after it is unreachable.

kotlin
fun log(msg: String): Unit { println(msg) }   // Unit usually omitted

fun fail(msg: String): Nothing = throw IllegalStateException(msg)

val name = user?.name ?: fail("no user")   // Nothing fits any type

Q19. What is a smart cast?

After you check a type with is (or check for null), the compiler automatically casts the value inside that scope, so you use it as the checked type without an explicit cast. This is the smart cast.

It works only when the compiler can prove the value can't change between the check and the use, which is why smart casts don't apply to mutable var properties that other code could reassign.

kotlin
fun describe(x: Any) {
    if (x is String) {
        println(x.length)   // smart cast: x is String here
    }
}

Q20. What is the difference between as and as? for casting?

as is an unsafe cast that throws a ClassCastException if the object isn't the target type. as? is a safe cast that returns null instead of throwing when the cast fails.

Pair as? with the Elvis operator to cast and provide a fallback in one line, which avoids both the crash and a separate type check.

kotlin
val obj: Any = "hello"
val s1 = obj as String        // ok
// val n = obj as Int         // throws ClassCastException
val n = obj as? Int ?: -1     // safe: n becomes -1

Q21. What is a single-expression function?

When a function body is one expression, you can drop the braces and the return keyword and use =. The return type is often inferred too, so the whole function fits on one line.

It reads well for small pure functions and mappers. For anything with multiple statements, use the block body with braces.

kotlin
fun square(n: Int) = n * n
fun fullName(first: String, last: String) = "$first $last"

Q22. What are Kotlin's visibility modifiers, and how do they differ from Java's?

Kotlin has public (the default), private, protected, and internal. The big differences from Java: public is the default instead of package-private, and internal means visible within the same compilation module, which Java has no equivalent for.

private at the top level means file-scoped, and protected in Kotlin is not visible to the same package (unlike Java).

ModifierVisible where
public (default)Everywhere
internalSame module
protectedThe class and its subclasses
privateThe class, or the file at top level

Q23. What is the difference between a primary and a secondary constructor?

The primary constructor is part of the class header and is where you declare and often initialize properties directly (class User(val name: String)). Secondary constructors are declared inside the body with the constructor keyword and must delegate to the primary one with this(...).

The init block holds setup logic that runs with the primary constructor. Most Kotlin classes need only a primary constructor plus default arguments, so secondary constructors are less common than in Java.

kotlin
class User(val name: String, val age: Int) {
    init {
        require(age >= 0) { "age must be non-negative" }
    }
    constructor(name: String) : this(name, 0)   // delegates to primary
}

Q24. How can the Elvis operator return or throw early?

Because return and throw are expressions of type Nothing, they fit on the right side of ?:. So val id = user?.id ?: return handles the null case by leaving the function, and ?: throw does the same with an exception.

This flattens nested null checks into a straight line of guard clauses, which reads better than deep if nesting.

kotlin
fun process(user: User?) {
    val name = user?.name ?: return          // bail out if null
    val email = user.email ?: throw IllegalStateException("no email")
    send(name, email)
}

Q25. Does Kotlin have checked exceptions?

No. Kotlin has no checked exceptions, so no function forces callers to catch or declare what it might throw, and there's no throws clause you must satisfy. Every exception is effectively unchecked.

The reasoning is that checked exceptions in large Java codebases often led to empty catch blocks or blanket rethrows that added noise without safety. You still use try/catch; you just aren't compelled to by the compiler.

kotlin
fun parse(s: String): Int {
    // no "throws" needed, caller is not forced to catch
    return s.toInt()   // may throw NumberFormatException at runtime
}
Back to question list

Kotlin Intermediate Interview Questions

Intermediate20 questions

For candidates with working experience: language mechanics, the standard library, and the questions that separate users from understanders.

Q26. What are extension functions and how are they resolved?

An extension function adds a method to an existing type without inheriting from it or editing its source. You write fun String.shout() = uppercase() and then call it as if String defined it.

They're resolved statically at compile time based on the declared type of the receiver, not the runtime type. So they don't override members and can't participate in polymorphism, which is the usual follow-up trap.

kotlin
fun String.shout(): String = this.uppercase() + "!"

println("hire me".shout())   // "HIRE ME!"

// compiles to a static method taking the receiver as first argument

Key point: The follow-up is 'do extension functions override class methods?' the technical answer is no: static resolution, member wins over extension.

Q27. What are higher-order functions and lambdas in Kotlin?

A higher-order function takes a function as a parameter or returns one. Kotlin has first-class function types like (Int) -> Int, so functions pass around like any value.

Lambdas are the concise literal syntax. When a lambda is the last argument, it moves outside the parentheses (trailing lambda), which is why builders like list.map { } read so cleanly. A single parameter is available as it.

kotlin
fun apply(x: Int, op: (Int) -> Int): Int = op(x)

val doubled = apply(5) { it * 2 }   // trailing lambda, it = 5
val names = listOf("a", "bb").map { it.length }  // [1, 2]

Q28. What are the scope functions let, run, with, apply, and also?

These five run a block on an object and differ in two ways: what the block sees the object as (it or this) and what the block returns (the object or the lambda result). let and also expose it; run, with, and apply expose this. apply and also return the object; let, run, and with return the lambda result.

In practice: let for null-safe transforms, apply for configuring an object, also for side effects like logging, run for computing a result from an object.

kotlin
val user = User("Asha", 30).apply {
    // configure: this = the User
}

name?.let { println(it.uppercase()) }   // runs only if name != null
FunctionObject refers toReturns
letitLambda result
runthisLambda result
withthisLambda result
applythisThe object
alsoitThe object

Key point: Nobody expects you to recite all five cold, but knowing the it-vs-this and return-value axes lets you derive any of them on the spot.

Q29. What are coroutines and how do they differ from threads?

A coroutine is a lightweight unit of concurrency you can suspend and resume without blocking a thread. Many coroutines run on a small pool of threads, so you can have hundreds of thousands where you'd have far fewer threads.

The key difference: suspension is cooperative and cheap, while a blocked thread holds an OS resource. Coroutines make async code read top to bottom, like sequential code, instead of nested callbacks.

kotlin
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000)          // suspends, does not block the thread
        println("world")
    }
    println("hello")
}
// prints: hello then world

Key point: Say 'suspend, not block' and 'cheaper than threads'. Those two phrases signal you understand why coroutines exist.

Watch a deeper explanation

Video: WHAT IS A COROUTINE? - Kotlin Coroutines (Philipp Lackner, YouTube)

Q30. What is a suspend function and where can you call it?

A suspend function can pause execution and resume later without blocking the calling thread. The suspend keyword tells the compiler to transform it so it can save and restore state at each suspension point.

You can only call a suspend function from another suspend function or from inside a coroutine builder like launch, async, or runBlocking. Calling one from ordinary code is a compile error, which is how the compiler enforces the coroutine boundary.

kotlin
suspend fun fetchUser(): User {
    delay(500)              // suspension point
    return User("Asha", 30)
}

// must be called inside a coroutine:
launch { val u = fetchUser() }

Watch a deeper explanation

Video: Kotlin Coroutine (High-quality Course) (Smartherd, YouTube)

Q31. What is the difference between launch and async?

launch starts a coroutine that doesn't return a result and gives you a Job to manage its lifecycle (cancel, join). Use it for fire-and-forget work. async starts a coroutine that computes a value and returns a Deferred<T>; call await() to get the result.

The pattern for parallel work is starting several asyncs, then awaiting them all, so the tasks overlap instead of running one after another.

kotlin
val job = launch { saveToDisk() }   // no result, returns Job

val a = async { fetchA() }          // returns Deferred
val b = async { fetchB() }
val total = a.await() + b.await()   // both ran concurrently

Key point: The strong follow-up answer is the two-async-then-await pattern for concurrency. Volunteer it.

Q32. What is structured concurrency in coroutines?

Structured concurrency means every coroutine runs inside a scope, and the scope won't finish until all its child coroutines finish. Cancelling the scope cancels the children, and a failing child cancels its siblings by default.

This prevents leaked coroutines and lost exceptions. coroutineScope { } and the lifecycle-tied scopes on Android are how you get it in practice.

How a coroutine scope manages its children

1Enter a scope
coroutineScope or a lifecycle scope opens
2Launch children
child coroutines start inside the scope
3Wait for all
the scope suspends until every child completes
4Cancel propagates
cancel or failure tears the whole scope down together

This is why a coroutine started in a scope can't outlive it: no leaked work, no swallowed errors.

Q33. What is a sealed class and when do you use one?

A sealed class defines a closed set of subtypes known at compile time; all direct subclasses must be declared in the same module. It models a fixed set of states, like a network result that's Loading, Success, or Error.

The payoff is exhaustive when: because the compiler knows every subtype, a when over a sealed class needs no else branch, and adding a new subtype turns forgotten cases into compile errors.

kotlin
sealed class Result
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()

fun render(r: Result) = when (r) {   // no else needed
    is Success -> show(r.data)
    is Error -> showError(r.message)
    Loading -> showSpinner()
}

Key point: Exhaustive when with no else is the whole point. the key point is that adding a case becomes a compile error.

Q34. What is the difference between a sealed class and an enum?

An enum is a fixed set of single instances that all share the same shape. A sealed class is a fixed set of subtypes that can each carry different data and have multiple instances. So enum values are constants; sealed subtypes are types.

Reach for an enum when the cases are just labels (days, directions). Reach for a sealed class when each case needs its own fields, like Success(data) versus Error(message).

EnumSealed class
Instances per caseOne (a constant)Many, each with state
Different data per caseNo (same fields)Yes
Best forFixed labelsFixed set of typed states

Q35. What does the object keyword do?

object creates a singleton: one instance, defined and instantiated in a single declaration, thread-safe by the language. Use it for stateless helpers, a single shared registry, or a manager where one instance is correct.

The same keyword also makes anonymous objects (object : Listener { }) and companion objects inside a class. All three are the same idea: an object without a reusable class.

kotlin
object Config {
    val version = "1.0"
    fun describe() = "v$version"
}

println(Config.describe())   // single instance, no constructor call

Q36. Which collection operations should every Kotlin developer know?

The functional pipeline set: map (transform), filter (keep matching), reduce and fold (accumulate), groupBy (bucket by key), associate (build a map), and flatMap (map then flatten). They chain into readable pipelines that replace manual loops.

Know that these run eagerly on collections, building an intermediate list at each step. For large chains, asSequence() makes them lazy, which is the natural follow-up.

kotlin
val nums = listOf(1, 2, 3, 4, 5, 6)
val result = nums.filter { it % 2 == 0 }
                  .map { it * it }
                  .sum()          // 4 + 16 + 36 = 56

val byParity = nums.groupBy { if (it % 2 == 0) "even" else "odd" }

Q37. What is the difference between a Sequence and a List for chained operations?

Operations on a List are eager: each step (filter, map) builds a full intermediate list before the next runs. A Sequence is lazy: elements flow through the whole chain one at a time, and nothing runs until a terminal operation like toList() or sum().

For short chains on small data, lists are fine and simpler. For long chains or large data, sequences avoid the intermediate allocations and can short-circuit early with operations like first.

kotlin
val result = (1..1_000_000).asSequence()
    .map { it * 2 }
    .filter { it % 3 == 0 }
    .first()          // stops as soon as it finds one; no huge lists built

Key point: The measurable point is 'no intermediate lists, can short-circuit.' Mention that sequences aren't always faster on small data.

Q38. How do generics work in Kotlin, and what are in and out?

Generics let a class or function work over a type parameter, like List<T>. Variance controls subtyping between generic types. out marks a producer: the type is only returned, so List<Dog> can be treated as List<Animal> (covariance). in marks a consumer: the type is only accepted, giving contravariance.

The mnemonic is producer-out, consumer-in. Where you can't annotate the class, a type projection at the use site (List<out Animal>) does the same job.

kotlin
interface Source<out T> {   // only produces T
    fun next(): T
}

val dogs: Source<Dog> = getDogs()
val animals: Source<Animal> = dogs   // ok because of out

Key point: Say 'producer-out, consumer-in.' That mnemonic plus one covariance example covers what most interviewers probe here.

Q39. What are reified type parameters?

Normally type arguments are erased at runtime, so you can't check T directly. Marking an inline function's type parameter reified keeps the actual type available at runtime, letting you write x is T or T::class inside the function.

It works only on inline functions, because the compiler substitutes the real type at each call site. It's what makes helpers like a type-safe fromJson<User>() possible.

kotlin
inline fun <reified T> Any.isType(): Boolean = this is T

println("hi".isType<String>())   // true
println("hi".isType<Int>())      // false

Q40. What are inline functions and why do they exist?

An inline function has its body, and its lambda arguments, copied into each call site at compile time instead of being called as separate objects. This removes the object allocation and call overhead that lambdas would otherwise add.

The main wins: cheaper higher-order functions in hot code, non-local returns from lambdas, and reified type parameters. The cost is larger bytecode, so inlining big functions is counterproductive.

kotlin
inline fun measure(block: () -> Unit) {
    val start = System.nanoTime()
    block()                       // copied in, no lambda object
    println("${System.nanoTime() - start} ns")
}

Q41. What is the difference between lateinit and lazy?

lateinit var promises a non-null var will be assigned before first use; it skips an initial value and throws if you read it too early. It's for var properties set by a framework or setup method, and only for non-primitive types.

by lazy { } computes a val's value on first access and caches it, thread-safe by default. Use lazy for expensive values you may not need; use lateinit for dependency-injected or framework-initialized fields.

kotlin
lateinit var service: Service        // set later, must be var, non-null

val config: Config by lazy {
    println("computed once")
    loadConfig()                     // runs on first access only
}

Key point: The clean distinction: lateinit is a mutable var you'll set later; lazy is a val computed on first read. State that and you've answered it.

Q42. What is delegation with the by keyword?

The by keyword delegates work to another object. Class delegation (class A(b: B) : Interface by b) forwards interface methods to a wrapped instance, giving composition without inheritance boilerplate. Property delegation routes get and set through a delegate that implements getValue and setValue.

lazy, observable, and Map-backed properties are all built on property delegation, so understanding by explains how those work.

kotlin
interface Repo { fun load(): String }
class RealRepo : Repo { override fun load() = "data" }

class Cached(repo: Repo) : Repo by repo   // forwards load() to repo

Q43. How does destructuring work in Kotlin?

Destructuring unpacks an object into several variables in one line: val (name, age) = user. It calls the object's component1(), component2(), and so on, which data classes generate automatically.

It shines when iterating maps (for ((key, value) in map)) and returning multiple values via a data class or Pair. Use an underscore to skip a component you don't need.

kotlin
val (name, age) = User("Asha", 30)

for ((key, value) in mapOf("a" to 1)) {
    println("$key -> $value")
}

val (_, second) = Pair("x", "y")   // skip the first

Q44. What are infix functions and operator overloading?

An infix function is called without the dot and parentheses (1 to 2 instead of 1.to(2)); it must be a single-parameter member or extension marked infix. Operator overloading maps symbols like +, [], and in to functions named plus, get, contains, and so on via the operator modifier.

Both improve readability for DSL-like APIs, but overusing them hurts clarity, so reserve operators for meanings people already expect.

kotlin
data class Vec(val x: Int, val y: Int) {
    operator fun plus(o: Vec) = Vec(x + o.x, y + o.y)
}

val sum = Vec(1, 2) + Vec(3, 4)   // Vec(4, 6)
val pair = "key" to "value"       // infix function to()

Q45. How does try/catch/finally work in Kotlin, and how is it an expression?

try runs code that may throw, catch handles a specific exception type, and finally always runs for cleanup. Kotlin has no checked exceptions, so you catch what you choose to.

The twist is that try is an expression: it returns the value of the try block, or of the matching catch block if one fires. So you can assign the result of a try directly, which fits Kotlin's expression-oriented style.

kotlin
val number: Int = try {
    input.toInt()
} catch (e: NumberFormatException) {
    0                     // try returns this on failure
} finally {
    println("parse attempted")
}

How a try expression resolves to a value

1Run the try block
its value becomes the result if nothing throws
2On throw, match a catch
the matching catch block's value becomes the result
3No match rethrows
an unhandled exception propagates up the call stack
4finally always runs
cleanup executes on every path, but does not change the result

finally runs whether the try succeeded, was caught, or is propagating, but its own value is ignored.

Back to question list

Kotlin Interview Questions for Experienced Developers

Experienced15 questions

advanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.

Q46. What is a CoroutineContext, and what do dispatchers do?

A CoroutineContext is a set of elements that configure a coroutine: its Job, its dispatcher, an optional name, and an exception handler. It's inherited by children and combined with the + operator.

The dispatcher decides which thread or pool runs the coroutine. Dispatchers.Default is for CPU work, Dispatchers.IO for blocking I/O with a larger pool, and Dispatchers.Main for the UI thread. withContext switches dispatcher for a block, which is how you move blocking work off the main thread.

kotlin
suspend fun loadName(): String = withContext(Dispatchers.IO) {
    // runs on the IO pool, off the main thread
    readFromDisk()
}
DispatcherBacked byUse for
DefaultCPU-sized poolCPU-bound work, sorting, parsing
IOLarger elastic poolBlocking I/O: disk, network, DB
MainUI threadTouching UI on Android/desktop

Key point: The follow-up is 'why not run blocking I/O on Default?' Answer: IO has a larger pool sized for waiting, Default is sized for CPU cores.

Q47. How does coroutine cancellation work, and how do you make code cancellable?

Cancellation is cooperative. Cancelling a coroutine sets a flag and throws CancellationException at the next suspension point. Suspending functions in kotlinx.coroutines check for cancellation, but a tight CPU loop with no suspension won't notice.

To stay cancellable in long computations, call ensureActive() or yield() periodically, or check isActive. Also, run cleanup in finally, and if you must suspend during cleanup, wrap it in withContext(NonCancellable).

kotlin
val job = launch {
    try {
        while (isActive) {        // cooperates with cancellation
            doChunk()
        }
    } finally {
        cleanup()                 // runs on cancel
    }
}
job.cancelAndJoin()

Q48. What is a Flow, and how does it differ from a suspend function and from a Channel?

A Flow is an asynchronous stream of multiple values produced over time, built on coroutines. A suspend function returns one value; a Flow emits many. Flows are cold by default: the producer block runs fresh for each collector and only when collection starts.

A Channel is hot and shares emissions between receivers, more like a queue for communicating between coroutines. Flow is for declarative streams; Channel is for passing values between running coroutines. SharedFlow and StateFlow are the hot, multicast variants of Flow.

kotlin
fun numbers(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)          // emits over time
    }
}

launch { numbers().collect { println(it) } }   // 1, 2, 3

Key point: Say 'cold, restarts per collector' for Flow and 'hot, shared' for Channel and StateFlow. That axis is what senior questions probe.

Watch a deeper explanation

Video: Flow Basics - The Ultimate Guide to Kotlin Flows (Part 1) (Philipp Lackner, YouTube)

Q49. How does exception handling work in coroutines?

An exception in a launch coroutine propagates up through the Job hierarchy and, by default, cancels the parent and siblings (structured concurrency). In async, the exception is stored in the Deferred and re-thrown when you call await().

A CoroutineExceptionHandler in the context catches uncaught exceptions from launch at the top level. To stop one child's failure from cancelling siblings, use a SupervisorJob or supervisorScope, where children fail independently.

kotlin
supervisorScope {
    launch { mayFail() }     // failure here does not cancel the sibling
    launch { alsoRuns() }
}

Key point: The distinction the question needs: normal scope cancels siblings on failure, supervisor scope isolates them. Both matter.

Q50. What do noinline and crossinline mean on inline function parameters?

In an inline function, lambda parameters are inlined by default. noinline opts a specific lambda out of inlining, which you need when you want to store it or pass it on as an object. crossinline keeps the lambda inlined but forbids non-local returns from it, which matters when the lambda is invoked from another execution context like a nested runnable.

The reason both exist is that inlining changes what a lambda can do: an inlined lambda can return from the enclosing function, and that isn't always safe or possible.

kotlin
inline fun run2(
    a: () -> Unit,
    noinline b: () -> Unit,     // kept as an object, can be stored
    crossinline c: () -> Unit   // inlined but no non-local return
) {
    a()
    val stored = b
    val r = Runnable { c() }
}

Q51. What is a value class (inline class), and why use one?

A value class wraps a single value and, where the compiler can, represents it at runtime as the underlying value with no wrapper object allocated. So you get a distinct type for type safety without the boxing cost.

The classic use is stopping primitive-obsession bugs: UserId and OrderId can both wrap Long yet be different types, so you can't pass one where the other is expected. The wrapper disappears at runtime in most cases.

kotlin
@JvmInline
value class UserId(val value: Long)

fun load(id: UserId) { }
// load(OrderId(5))   // compile error: wrong type, no runtime cost

Q52. How does Kotlin interoperate with Java, and where does it get sharp?

Kotlin compiles to the same JVM bytecode as Java, so each can call the other directly. Kotlin adds annotations to smooth the seam: @JvmStatic exposes companion members as Java statics, @JvmOverloads generates overloads for default arguments, and @JvmField exposes a property as a plain field.

The sharp edge is platform types. Java values have unknown nullability, so Kotlin treats them as platform types (String!) and can't enforce null safety on them. If Java returns null into a Kotlin non-null type, you get a runtime failure at the boundary. Annotating the Java side or checking at the boundary is the fix.

Key point: Platform types are the answer this question is fishing for. Mention that Kotlin's null guarantee stops at the Java boundary.

Q53. How is Kotlin null safety enforced given the JVM has no such concept?

Most of it is compile-time: the compiler tracks nullable versus non-null types and rejects unsafe uses, so there's no runtime type distinction for that part. On top of that, the compiler inserts runtime null checks at boundaries, notably intrinsic checks on public function parameters, so a null slipping in from Java fails fast with a clear message.

So null safety is a compiler feature backed by defensive runtime checks at the edges, not a JVM type-system change.

Q54. How do you write a custom property delegate?

Implement getValue (and setValue for a var) with the delegate signatures, then attach it with by. The delegate receives the owning object and the property metadata, so one delegate can serve many properties and add behavior like validation, logging, or caching.

This is the same mechanism behind lazy and Delegates.observable, so writing one shows you understand what by actually does.

kotlin
import kotlin.reflect.KProperty

class NonBlank {
    private var v = ""
    operator fun getValue(thisRef: Any?, p: KProperty<*>) = v
    operator fun setValue(thisRef: Any?, p: KProperty<*>, value: String) {
        require(value.isNotBlank()) { "${p.name} must not be blank" }
        v = value
    }
}

var name: String by NonBlank()

Q55. What is the contract between equals and hashCode, and how do data classes handle it?

Objects that are equal by equals() must return the same hashCode(), and hashCode must stay stable while the object is used as a map key or set member. Break it and objects vanish inside hash maps.

Data classes generate both from the primary-constructor properties, so they satisfy the contract automatically. The trap is a data class with a mutable var in the constructor used as a map key: mutating it changes the hash and corrupts the map, which is why value objects should be immutable.

Key point: The senior nuance is the mutable-key hazard. it in practice matters.

Q56. What is Kotlin Multiplatform and how does expect/actual work?

Kotlin Multiplatform lets you share code across targets (JVM, Android, iOS, JS, native) by writing common logic once and compiling it to each platform. You share business logic while keeping platform-specific UI native.

The expect/actual mechanism handles platform differences: common code declares expect fun currentTime() with no body, and each platform provides an actual implementation. The compiler binds them per target, so shared code calls one API backed by platform-specific code.

kotlin
// commonMain
expect fun platformName(): String

// androidMain
actual fun platformName() = "Android"

// iosMain
actual fun platformName() = "iOS"

Q57. What does the tailrec modifier do?

tailrec tells the compiler to rewrite a tail-recursive function into a loop, so deep recursion doesn't overflow the stack. It applies only when the recursive call is the very last operation in the function, with nothing left to do after it returns.

If the call isn't truly in tail position, the compiler warns and leaves it as ordinary recursion. It's how you write a recursive algorithm safely on the JVM, which has no native tail-call elimination.

kotlin
tailrec fun factorial(n: Long, acc: Long = 1): Long =
    if (n <= 1) acc else factorial(n - 1, acc * n)
// compiled to a loop: no stack growth

Q58. How does Kotlin support building type-safe DSLs?

Kotlin DSLs combine three features: lambdas with receiver (a lambda whose this is a builder object), trailing lambda syntax, and extension functions. Together they let a config block read like structured markup while staying fully type-checked.

@DslMarker prevents accidental access to an outer builder's scope from an inner block, which keeps nested builders unambiguous. This is how libraries like the kotlinx.html and Gradle Kotlin DSLs work.

kotlin
class Html { fun body(init: Body.() -> Unit) { Body().init() } }
class Body { fun p(text: String) { } }

fun html(init: Html.() -> Unit) = Html().apply(init)

html {
    body { p("hello") }   // lambda with receiver: this = Body
}

Q59. Why do smart casts fail on some properties, and what are contracts?

Smart casts need the compiler to prove a value can't change between the check and the use. They fail on mutable var properties, especially open ones or ones from another module, because other code could reassign them after the check. The fix is copying into a local val first.

Contracts let a function tell the compiler about its effects, for example that returning true implies an argument is non-null, so smart casts and definite-assignment analysis work across your own helper functions.

kotlin
class Box { var value: String? = null }

fun use(box: Box) {
    val v = box.value          // copy to a local val
    if (v != null) println(v.length)   // smart cast works on the local
}

Q60. What performance costs are hidden in idiomatic Kotlin, and how do you address them?

The usual suspects: non-inlined lambdas allocate objects, boxing happens when generics or nullable primitives (Int?) force wrapper types, and long eager collection chains build an intermediate list at every step. None of these matter until a profiler says they do.

The fixes map one to one: inline hot higher-order functions, use value classes or primitive arrays (IntArray) to dodge boxing, and switch heavy chains to sequences. As always, measure first with a JVM profiler, because Kotlin's costs are the JVM's costs plus a thin layer.

Key point: This is a measure-first question. The strong coverage names the specific costs but insists on profiling before changing anything.

Back to question list

Why Kotlin? Kotlin vs Java, Scala, and Swift

Kotlin wins when you want Java's ecosystem without Java's verbosity and null holes, which describes most Android and JVM backend work today. It trades a slightly bigger compiler and some IDE dependence for null safety, concise syntax, and coroutines. Where it competes, Scala reaches for a richer functional type system at the cost of a steeper learning curve, and Swift covers the same concise-and-safe ground but only inside Apple's world. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.

LanguageTypingRuns onBest atWatch out for
KotlinStatic, null-awareJVM, JS, NativeAndroid, JVM backends, Java interopCompile times, tooling dependence
JavaStaticJVMHuge ecosystem, long-term stabilityVerbosity, null holes
ScalaStaticJVM, JSData engineering, deep functional codeSteep learning curve, complexity
SwiftStatic, null-awareApple, LinuxiOS and Apple platformsWeak reach off Apple platforms

How to Prepare for a Kotlin Interview

Prepare in layers, and practice out loud. Most Kotlin rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.

  • Master your tier's concepts until you can explain them without notes, then read one tier up for the stretch questions.
  • Type and run every snippet in the Kotlin Playground; modifying working code cements it far faster than reading.
  • Practice thinking aloud on small problems with a timer, because your process, not just the final answer is the technical point.
  • Take the quiz on this page to find weak spots, then revisit those questions before the real round.

The typical Kotlin interview flow

1Recruiter or phone screen
background, motivation, a few concept checks
2Technical concepts
null safety, val vs var, data classes, coroutines
3Live coding
write and explain a function under observation
4Design or debugging
trade-offs, reading unfamiliar code, follow-ups

Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.

Test Yourself: Kotlin Quiz

Ready to test your Kotlin knowledge?

10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.

10 questions Instant feedback Free certificate on 70%+

Frequently  Asked  Questions

Which Kotlin topics should I prioritize before a technical round?

Prioritize the areas that change real decisions: fundamentals, practical tasks, debugging, trade-offs, and evidence. The highest-value questions are tied to a project example, a validation check, or a failure mode.

Are these questions enough to pass a Kotlin interview?

They cover the question-answer portion well, but most Kotlin rounds also include live coding: writing a function under observation while explaining your thinking. solving small problems out loud with a timer is the practice path. Our AI coding interview prep guide covers that format, including how automated evaluation reads your process.

Do I need to know Java for a Kotlin interview?

Some Java helps because Kotlin runs on the JVM and interops with Java, so questions about bytecode, exceptions, and interop come up. You don't need to write Java fluently, but knowing how a Kotlin feature maps to Java (data classes to POJOs, extension functions to static methods) indicates real depth.

How long does it take to prepare for a Kotlin interview?

If you use Kotlin at work or in study, one to two weeks of an hour a day covers this bank with practice runs. Starting colder, plan three to four weeks and write code daily; reading answers without typing them is how preparation quietly fails.

Do interviewers ask exactly these questions?

The concepts repeat far more reliably than the exact wording. Interviewers rephrase, chain follow-ups, and push toward whatever you claim confidence in. Prepare the underlying ideas, null safety, scope functions, coroutines, sealed classes, and the phrasing takes care of itself.

Is there a way to test my Kotlin knowledge?

Yes. The quiz on this page scores you as you go and explains every answer, so you find your weak spots fast. Pass the threshold and you can download a shareable certificate with your name and score, free and with no sign-up. It's the quickest way to check yourself before the real round.

From the team that builds coding interviews

Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Kotlin questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.

See how the AI Coding Interviewer works

Sources

Adithyan RKWritten by Adithyan RK
Surya N
Fact-checked by Surya N
Published on: 29 Apr 2026Last updated: 8 Jul 2026
Share: