Kotlin Basics
Irbisa · cheatsheetSeptember 13, 2026

Kotlin Basics

val/var, types, null safety, control flow, functions, lambdas

Junior Developer20 itemscompressed for a skim
  1. 01

    What is the difference between val and var in Kotlin?

    Easy

    val fixes the reference while var allows 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                // ✅
    …
  2. 02

    How does null safety work in Kotlin?

    Easy

    Nullability is part of the type in Kotlin: String can 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"
    …
  3. 03

    Is Kotlin's List immutable? How do read-only and mutable collection types differ?

    Easy

    Kotlin's List is 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" }
    …
  4. 04

    Explain functions, lambdas, and higher-order functions in Kotlin.

    Easy

    Kotlin 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
    …
  5. 05

    When is if/when an expression in Kotlin, and what does that require?

    Medium

    In 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"
    …
  6. 06

    What are smart casts in Kotlin and when do they work?

    Medium

    A 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
    …
  7. 07

    In Kotlin, what is the difference between == and ===?

    Easy

    == compares content by calling equals(), 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)
    …
  8. 08

    A property cannot be initialized in the constructor. When do you reach for lateinit and when for by lazy?

    Easy

    lateinit var is for a value somebody else hands you later, by lazy is 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())
    …
  9. 09

    How do destructuring declarations work in Kotlin, and what breaks when a data class changes?

    Easy

    Destructuring is positional — val (a, b) = obj calls the component1() and component2() 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. 10

    Why does val total: Long = itemCount fail to compile when itemCount is an Int?

    Easy

    Kotlin 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. 11

    Can a val in Kotlin return a different value every time you read it?

    Medium

    Yes — a val with 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. 12

    Kotlin has no checked exceptions. How do you signal and handle failure then?

    Medium

    Nothing 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. 13

    A profile screen renders User(name=Alice).name instead of the name — what did the string template do?

    Easy

    $ binds to the shortest identifier after it, so "$user.name" interpolates the whole user object 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. 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, so 0..items.size runs one step past the last valid index; the open-ended forms are 0..<items.size (the ..< operator, stable since Kotlin 1.9) and the older 0 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. 15

    list.first() crashed with NoSuchElementException on an empty response — which stdlib naming rule did you miss?

    Easy

    The plain name asserts that the element exists and throws when it does not; the OrNull suffix 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. 16

    You add force: Boolean = false to a widely-called function — what changes for subclasses, Java callers and already-compiled modules?

    Medium

    A 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. 17

    You wrote return inside items.forEach { } to skip one item and the whole function stopped running — why?

    Medium

    forEach is an inline function, so its lambda body is compiled into your function and a bare return is a real return from your function — skipping a single item is return@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. 18

    Why does val id: String = raw ?: throw IllegalStateException() type-check when throw produces no value at all?

    Medium

    throw is an expression of type Nothing, the bottom of Kotlin's type hierarchy and a subtype of every other type, so it fits into a branch that has to produce a String.

    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. 19

    Two Array<String> values with identical contents compare as unequal and a data class holding one has broken equality — why?

    Medium

    Arrays inherit Java's identity equals, so == on two arrays compares references, not contents — content comparison is contentEquals (contentDeepEquals for 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. 20

    if (user.isPremium) stops compiling once the JSON field arrives as Boolean? — which fix quietly turns "unknown" into "no"?

    Medium

    if needs a real Boolean and Boolean? has three states, so you cannot write the condition until you decide what null means — and == true, ?: false both 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
    …