Kotlin Interview Questions: Coroutines, Extension Functions, and Android (2025)

Kotlin is the primary language for Android development and is increasingly used for backend services (Ktor, Spring Boot). Kotlin interviews go well beyond “how is it different from Java” — interviewers expect deep knowledge of coroutines, the type system, and idiomatic Kotlin patterns.

Null Safety

What is the difference between ?, !!, and ?: in Kotlin?

// ? makes a type nullable
var name: String? = null         // OK — name can be null
// var name: String = null       // Compile error — String is non-nullable

// Safe call operator: returns null if the receiver is null
val length = name?.length        // type: Int? (null if name is null)
val upper = name?.uppercase()    // null if name is null, otherwise uppercase string

// Elvis operator: provide a default if null
val len = name?.length ?: 0      // 0 if name is null
val result = getUserName() ?: "Anonymous"

// Not-null assertion: throws NullPointerException if null
val len2 = name!!.length         // Use sparingly — defeats null safety
// Only use !! when you are CERTAIN the value cannot be null and the compiler cannot prove it

// Let for null-safe scope
name?.let { n ->
    println("Name is: $n")       // n is guaranteed non-null inside let
}

Data Classes and Sealed Classes

// Data class: auto-generates equals, hashCode, toString, copy, componentN
data class User(
    val id: Long,
    val name: String,
    val email: String,
    val role: Role = Role.USER
)

// copy() creates a modified copy without mutating the original
val admin = user.copy(role = Role.ADMIN)

// Destructuring via componentN functions
val (id, name, email) = user

// Sealed class: exhaustive hierarchy — all subclasses must be in same file
sealed class ApiResult {
    data class Success(val data: T) : ApiResult()
    data class Error(val message: String, val code: Int) : ApiResult()
    object Loading : ApiResult()
}

// when is exhaustive — compiler ensures all cases handled
fun handleResult(result: ApiResult) = when (result) {
    is ApiResult.Success -> showUser(result.data)
    is ApiResult.Error   -> showError(result.message)
    ApiResult.Loading    -> showSpinner()
    // No else needed — sealed class is exhaustive
}

Extension Functions

// Extension function: add methods to existing classes without inheriting
fun String.toTitleCase(): String =
    split(" ").joinToString(" ") { word ->
        word.replaceFirstChar { it.uppercase() }
    }

fun Int.isEven(): Boolean = this % 2 == 0

fun List.median(): Double {
    val sorted = sorted()
    return if (sorted.size % 2 == 0) {
        (sorted[sorted.size / 2 - 1] + sorted[sorted.size / 2]) / 2.0
    } else {
        sorted[sorted.size / 2].toDouble()
    }
}

// Usage — looks like the method belongs to the class
"hello world".toTitleCase()  // "Hello World"
42.isEven()                  // true
listOf(1, 3, 5, 7).median()  // 4.0

// Extension functions with receivers — DSL building blocks
fun StringBuilder.appendLine(s: String): StringBuilder {
    append(s).append("n")
    return this
}

Coroutines

What are coroutines and how do they differ from threads?

Coroutines are lightweight, suspendable computations. Unlike threads (which are OS-level and expensive to create — ~1MB stack), coroutines are user-space constructs managed by the Kotlin runtime. You can run millions of coroutines on a few threads. Coroutines use suspend functions that can pause execution without blocking the underlying thread.

import kotlinx.coroutines.*

// launch: fire-and-forget coroutine (returns Job)
// async: returns a Deferred (like Future) with a result

fun main() = runBlocking {
    // Launch 10,000 coroutines — would fail with threads but not coroutines
    val jobs = (1..10_000).map { i ->
        launch {
            delay(1000L)  // suspends (does not block thread), resumes after 1s
            println("Coroutine $i done")
        }
    }
    jobs.joinAll()  // wait for all to complete
}

// suspend function: can be paused and resumed
suspend fun fetchUser(id: Long): User {
    return withContext(Dispatchers.IO) {     // run on IO thread pool
        httpClient.get("/users/$id")        // actual network call
    }
}

// Parallel execution with async/await
suspend fun fetchUserAndOrders(userId: Long): Pair<User, List> {
    return coroutineScope {
        val userDeferred   = async { fetchUser(userId) }
        val ordersDeferred = async { fetchOrders(userId) }
        // Both run in parallel
        Pair(userDeferred.await(), ordersDeferred.await())
    }
}

Coroutine Dispatchers

Dispatcher Thread Pool Use For
Dispatchers.Main Main/UI thread UI updates in Android
Dispatchers.IO Large pool (64+ threads) Network, file I/O, DB queries
Dispatchers.Default CPU-count threads CPU-intensive computation
Dispatchers.Unconfined No specific thread Testing, special cases

Structured Concurrency

// Structured concurrency: child coroutines cannot outlive their parent scope
// Cancellation and exception propagation is automatic

suspend fun processItems(items: List) = coroutineScope {
    items.map { item ->
        async {
            processItem(item)  // runs in parallel
        }
    }.awaitAll()
    // If any async throws, ALL other coroutines in this scope are cancelled
    // The exception propagates to the caller
}

// Flow: cold stream for sequences of values (reactive programming)
fun getTemperatureReadings(): Flow = flow {
    while (true) {
        emit(sensor.read())     // suspending emit
        delay(1000)
    }
}

// Collect flow values
scope.launch {
    getTemperatureReadings()
        .filter { it > 37.5f }      // only emit high temp
        .map { "${it}°C" }          // transform
        .collect { temp ->           // terminal operator — starts the flow
            showAlert("High temp: $temp")
        }
}

Higher-Order Functions and Lambdas

// Functions are first-class — pass them as parameters
fun  List.transform(transform: (T) -> R): List =
    map(transform)

listOf(1, 2, 3).transform { it * 2 }  // [2, 4, 6]

// Inline functions: eliminate lambda object allocation at the call site
inline fun  measureTime(block: () -> T): Pair {
    val start = System.nanoTime()
    val result = block()
    return Pair(result, System.nanoTime() - start)
}

// Trailing lambda syntax (Kotlin convention)
listOf(1, 2, 3, 4, 5)
    .filter { it % 2 == 0 }
    .map { it * it }
    .forEach { println(it) }
// Equivalent to: .forEach({ println(it) }) but cleaner

Kotlin vs Java Key Differences

Feature Kotlin Java
Null safety Compile-time nullable types Runtime NPE
Data classes One line with auto-generated methods Verbose + Lombok or records (Java 16+)
Extension functions Built-in Not available (use static utility methods)
Coroutines Built-in language feature CompletableFuture / Virtual Threads (Java 21+)
Sealed classes Exhaustive hierarchies Sealed classes added in Java 17
Smart casts Automatic type narrowing after instanceof check Manual cast required
String templates "Hello, $name!" Manual concatenation or formatted()

Frequently Asked Questions

What are Kotlin coroutines and how do they differ from threads?

Coroutines are lightweight, suspendable computations managed by the Kotlin runtime. Unlike OS threads (each consuming about 1MB of stack space), coroutines are user-space constructs with tiny initial stacks (a few hundred bytes) that grow on demand. You can run millions of coroutines on just a few threads. The key difference: when a coroutine suspends (on delay(), network I/O, or any suspend function), it releases the thread for other coroutines to use — the thread is never blocked. Threads, by contrast, block the OS thread during any blocking I/O call. This makes coroutines far more efficient for I/O-bound workloads like network servers, without requiring callback hell or complex state machines.

What is the difference between launch and async in Kotlin coroutines?

Both launch and async start a new coroutine. launch returns a Job (a handle to cancel or join the coroutine) and is used for fire-and-forget operations where you do not need a return value. async returns a Deferred<T> (like a Future/Promise) that holds the result — you call await() to suspend until the result is ready. Use async when you need to start multiple coroutines in parallel and collect their results: val a = async { fetchA() }; val b = async { fetchB() }; both run concurrently; val result = Pair(a.await(), b.await()) waits for both. Using two sequential launch calls or two sequential suspend function calls would be sequential, not parallel.

What are sealed classes in Kotlin and when should you use them?

A sealed class is a restricted class hierarchy where all direct subclasses must be defined in the same file (or package in Kotlin 1.5+). This makes the hierarchy closed and exhaustive. When you use sealed classes in a when expression, the compiler can verify that all possible subtypes are handled — no else branch needed. This is invaluable for modeling sum types: API result states (Loading, Success, Error), Redux-style actions, or any type that has a fixed set of variants. The alternative — using open classes or interfaces — does not give you exhaustiveness checking, so a forgotten case compiles fine but fails at runtime. Sealed classes are essentially Kotlin enums with the ability to carry data.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What are Kotlin coroutines and how do they differ from threads?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Coroutines are lightweight, suspendable computations managed by the Kotlin runtime. Unlike OS threads (each consuming about 1MB of stack space), coroutines are user-space constructs with tiny initial stacks (a few hundred bytes) that grow on demand. You can run millions of coroutines on just a few threads. The key difference: when a coroutine suspends (on delay(), network I/O, or any suspend function), it releases the thread for other coroutines to use — the thread is never blocked. Threads, by contrast, block the OS thread during any blocking I/O call. This makes coroutines far more efficient for I/O-bound workloads like network servers, without requiring callback hell or complex state machines.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between launch and async in Kotlin coroutines?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Both launch and async start a new coroutine. launch returns a Job (a handle to cancel or join the coroutine) and is used for fire-and-forget operations where you do not need a return value. async returns a Deferred (like a Future/Promise) that holds the result — you call await() to suspend until the result is ready. Use async when you need to start multiple coroutines in parallel and collect their results: val a = async { fetchA() }; val b = async { fetchB() }; both run concurrently; val result = Pair(a.await(), b.await()) waits for both. Using two sequential launch calls or two sequential suspend function calls would be sequential, not parallel.”
}
},
{
“@type”: “Question”,
“name”: “What are sealed classes in Kotlin and when should you use them?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A sealed class is a restricted class hierarchy where all direct subclasses must be defined in the same file (or package in Kotlin 1.5+). This makes the hierarchy closed and exhaustive. When you use sealed classes in a when expression, the compiler can verify that all possible subtypes are handled — no else branch needed. This is invaluable for modeling sum types: API result states (Loading, Success, Error), Redux-style actions, or any type that has a fixed set of variants. The alternative — using open classes or interfaces — does not give you exhaustiveness checking, so a forgotten case compiles fine but fails at runtime. Sealed classes are essentially Kotlin enums with the ability to carry data.”
}
}
]
}

Companies That Ask This Question

Scroll to Top