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.”
}
}
]
}