Kotlin Basics
val/var, types, null safety, control flow, functions, lambdas
- 01
What is the difference between val and var in Kotlin?
Easyvalfixes the reference whilevarallows reassignment, and neither says anything about whether the object itself can change.val name = "Alice" // type inferred String // name = "Bob" // ❌ compile error var counter = 0 counter += 1 // ✅ … - 02
How does null safety work in Kotlin?
EasyNullability is part of the type in Kotlin:
Stringcan never hold null,String?can, and the compiler enforces the difference.fun render(name: String?) { // Safe call println(name?.length) // null if name is null // Elvis — supply default val display = name ?: "Guest" … - 03
Is Kotlin's
Listimmutable? How do read-only and mutable collection types differ?EasyKotlin's
Listis read-only, not immutable — the interface simply has no mutators, while the object behind it may be a MutableList somebody else can still change.val numbers = listOf(1, 2, 3, 4, 5) val doubled = numbers.map { it * 2 } // [2,4,6,8,10] val evens = numbers.filter { it % 2 == 0 } // [2,4] val total = numbers.sum() // 15 val grouped = numbers.groupBy { if (it % 2 == 0) "even" else "odd" } … - 04
Explain functions, lambdas, and higher-order functions in Kotlin.
EasyKotlin functions are first-class values — you can pass them as arguments, return them, and store them in variables.
// Default + named args fun greet(name: String, greeting: String = "Hello") = "$greeting, $name" greet("Alice") // Hello, Alice greet("Bob", greeting = "Hi") // Hi, Bob // Higher-order … - 05
When is
if/whenan expression in Kotlin, and what does that require?MediumIn Kotlin control structures are expressions — they produce a value and can sit on the right-hand side of an assignment or be returned directly.
// if as expression val status: String = if (score >= 90) "A" else if (score >= 70) "B" else "F" // when on a value fun gradeLabel(score: Int) = when { score >= 90 -> "A" … - 06
What are smart casts in Kotlin and when do they work?
MediumA smart cast is the compiler narrowing a value's type after a runtime check, so you never write the cast yourself.
fun describe(x: Any) { if (x is String) { println(x.length) // smart cast — x is String here } if (x !is Int) return println(x + 1) // smart cast — x is Int here … - 07
In Kotlin, what is the difference between
==and===?Easy==compares content by callingequals(), while===asks whether two names point to the very same object.val a = User(1, "Ada") val b = User(1, "Ada") println(a == b) // true — data class equals println(a === b) // false — two objects data class User(val id: Int, val name: String) … - 08
A property cannot be initialized in the constructor. When do you reach for
lateinitand when forby lazy?Easylateinit varis for a value somebody else hands you later,by lazyis for a value the property can compute itself on first read.class ProfileActivity : AppCompatActivity() { private lateinit var binding: ProfileBinding // set in onCreate @Inject lateinit var repo: UserRepository // set by Hilt private val formatter by lazy { // built on first use SimpleDateFormat("dd MMM", Locale.getDefault()) … - 09
How do destructuring declarations work in Kotlin, and what breaks when a data class changes?
EasyDestructuring is positional —
val (a, b) = objcalls thecomponent1()andcomponent2()operators in order, and the names you write are ignored.data class Size(val width: Int, val height: Int) val (w, h) = Size(1080, 1920) // component1(), component2() for ((index, item) in listOf("a", "b").withIndex()) { println("$index -> $item") … - 10
Why does
val total: Long = itemCountfail to compile whenitemCountis anInt?EasyKotlin has no implicit widening between numeric types — every conversion is an explicit call such as
toLong().val itemCount: Int = 42 // val total: Long = itemCount // ❌ type mismatch val total: Long = itemCount.toLong() // ✅ val fromLiteral: Long = 1 // ✅ the literal is typed as Long val mixed = itemCount * 2L // Long — arithmetic promotes … - 11
Can a
valin Kotlin return a different value every time you read it?MediumYes — a
valwith a custom getter has no backing field and re-runs its body on every read, so only the absence of a setter is guaranteed.class Basket(private val items: MutableList<Item> = mutableListOf()) { val size: Int get() = items.size // no backing field, recomputed val isEmpty: Boolean get() = size == 0 var lastTouched: Long = 0L private set // public read, private write … - 12
Kotlin has no checked exceptions. How do you signal and handle failure then?
MediumNothing in a Kotlin signature tells the caller that a function can throw, so failure is either documented or moved into the return type.
// try/catch is an expression val port = try { raw.toInt() } catch (e: NumberFormatException) { 8080 } // Preconditions: argument vs invariant fun withdraw(amount: Long) { require(amount > 0) { "amount must be positive, was $amount" } … - 13
A profile screen renders
User(name=Alice).nameinstead of the name — what did the string template do?Easy$binds to the shortest identifier after it, so"$user.name"interpolates the wholeuserobject and then appends the literal text.name.data class User(val name: String) val user = User("Alice") println("$user.name") // ❌ "User(name=Alice).name" println("${user.name}") // ✅ "Alice" … - 14
Your
for (i in 0..items.size)loop crashes on the last iteration — how do Kotlin's range operators differ?Easy..is inclusive at both ends, so0..items.sizeruns one step past the last valid index; the open-ended forms are0..<items.size(the..<operator, stable since Kotlin 1.9) and the older0 until items.size.val items = listOf("a", "b", "c") // for (i in 0..items.size) println(items[i]) // ❌ IndexOutOfBounds on the last step for (i in 0..<items.size) println(items[i]) // ✅ end excluded (Kotlin 1.9+) for (i in 0 until items.size) println(items[i]) // ✅ same thing, older spelling for (i in items.indices) println(items[i]) // ✅ clearest when you need the index … - 15
list.first()crashed with NoSuchElementException on an empty response — which stdlib naming rule did you miss?EasyThe plain name asserts that the element exists and throws when it does not; the
OrNullsuffix is the question form that hands back null instead.fun render(rows: List<Row>) { val first = rows.firstOrNull() ?: return showEmptyState() // ✅ empty is expected here // val bad = rows.first() // ❌ NoSuchElementException val third = rows.getOrNull(2) val label = rows.getOrElse(2) { Row.Placeholder } … - 16
You add
force: Boolean = falseto a widely-called function — what changes for subclasses, Java callers and already-compiled modules?MediumA default value belongs to the declaration, not to the call: the Kotlin compiler fills it in at each call site, so overriding code, Java code and code compiled earlier all see a different function than you do.
open class Loader { open fun load( id: String, force: Boolean = false, retries: Int = if (force) 1 else 3, // a default may use an earlier parameter ) { /* ... */ } … - 17
You wrote
returninsideitems.forEach { }to skip one item and the whole function stopped running — why?MediumforEachis an inline function, so its lambda body is compiled into your function and a barereturnis a real return from your function — skipping a single item isreturn@forEach.fun logAll(items: List<Item>) { items.forEach { if (it.isHidden) return // ❌ exits logAll entirely on the first hidden item println(it.title) } println("done") // never printed once one item is hidden … - 18
Why does
val id: String = raw ?: throw IllegalStateException()type-check whenthrowproduces no value at all?Mediumthrowis an expression of typeNothing, the bottom of Kotlin's type hierarchy and a subtype of every other type, so it fits into a branch that has to produce aString.fun requireId(raw: String?): String = raw ?: throw IllegalStateException("no id") // `throw` is Nothing, a subtype of String fun loadConfig(): Config = TODO("wire the remote config") // TODO() is Nothing too fun fail(msg: String): Nothing = throw IllegalArgumentException(msg) … - 19
Two
Array<String>values with identical contents compare as unequal and a data class holding one has broken equality — why?MediumArrays inherit Java's identity
equals, so==on two arrays compares references, not contents — content comparison iscontentEquals(contentDeepEqualsfor nested arrays).val a = arrayOf("x", "y") val b = arrayOf("x", "y") println(a == b) // false — identity equals, inherited from Any println(a.contentEquals(b)) // true println(a.toString()) // [Ljava.lang.String;@1b6d3586 … - 20
if (user.isPremium)stops compiling once the JSON field arrives asBoolean?— which fix quietly turns "unknown" into "no"?Mediumifneeds a realBooleanandBoolean?has three states, so you cannot write the condition until you decide what null means — and== true,?: falseboth decide it means false.data class UserDto(val id: String, val isPremium: Boolean?) // JSON may omit the field fun render(user: UserDto) { // if (user.isPremium) { } // ❌ Boolean? is not a condition if (user.isPremium == true) { } // null -> false if (user.isPremium != false) { } // null -> true …