Kotlin Advanced
Irbisa · cheatsheetSeptember 13, 2026

Kotlin Advanced

Generics, inline/reified, delegates, DSL builders, type aliases

Middle Developer20 itemscompressed for a skim
  1. 01

    How do generics, variance, and out/in modifiers work in Kotlin?

    Medium

    Kotlin declares variance at the declaration site: out marks a type parameter the class only produces, in one it only consumes.

    // Covariance — produce T
    interface Producer<out T> { fun produce(): T }
    val dogs: Producer<Dog> = object : Producer<Dog> { override fun produce() = Dog() }
    val animals: Producer<Animal> = dogs   // ✅ covariant
    
    // Contravariance — consume T
    …
  2. 02

    What are inline functions and reified type parameters in Kotlin?

    Medium

    An inline fun is replaced at compile time with the body of the function copied into the call site.

    // Allocation-free higher-order
    inline fun measure(label: String, block: () -> Unit) {
      val start = System.nanoTime()
      block()
      println("\$label: \${(System.nanoTime() - start)/1e6} ms")
    }
    …
  3. 03

    What are property delegates? Explain by lazy, by Delegates.observable, custom delegates.

    Medium

    Property delegation hands a property's get and set to another object: by forwards every read and write to that object's getValue / setValue.

    import kotlin.properties.Delegates
    import kotlin.properties.ReadWriteProperty
    import kotlin.reflect.KProperty
    
    class Repository {
      val client: HttpClient by lazy { buildHttpClient() }   // built once on first access
    …
  4. 04

    How do you write a type-safe DSL in Kotlin?

    Hard

    A type-safe Kotlin DSL rests on three features: lambdas with receiver, extension functions and the @DslMarker annotation.

    @DslMarker
    annotation class HtmlDsl
    
    @HtmlDsl
    class Html { val children = mutableListOf<Tag>() }
    …
  5. 05

    What are typealiases and when should you use them?

    Medium

    typealias introduces a name for an existing type.

    typealias UserId = Long
    typealias Cache = MutableMap<String, ByteArray>
    typealias EventHandler = (Event) -> Unit
    
    class Authentication { fun login(): UserId = 42L }
    …
  6. 06

    What are value classes (inline classes) in Kotlin?

    Hard

    A value class wraps a single value that the compiler erases at runtime, replacing the wrapper with the underlying value wherever it can.

    @JvmInline
    value class Email(val value: String) {
      init { require("@" in value && "." in value) { "Invalid email" } }
    }
    
    @JvmInline
    …
  7. 07

    Your Kotlin module is consumed from Java. Which declarations need a @Jvm... annotation to look right on the other side?

    Medium

    Companion objects, default arguments, top-level functions and property annotations all compile into shapes a Java caller finds awkward, and the @Jvm family exists to fix each one.

    @file:JvmName("Strings")
    
    package com.app.util
    
    fun slugify(input: String): String = input.lowercase().replace(' ', '-')
    // Java: Strings.slugify("Hello There")
    …
  8. 08

    What are context parameters in Kotlin, and what problem do they solve that an extra function argument does not?

    Hard

    A context parameter is a dependency a function declares but nobody passes at the call site — the compiler supplies it from whatever context is in scope.

    interface Logger { fun info(msg: String) }
    interface Analytics { fun track(event: String) }
    
    context(logger: Logger)
    fun sync() {
      logger.info("sync started")
    …
  9. 09

    Why does a smart cast survive if (!s.isNullOrEmpty()) but disappear through an identical helper you wrote yourself?

    Hard

    The standard library function declares a contract, a machine-readable promise that tells the compiler what its return value implies about its arguments.

    @OptIn(ExperimentalContracts::class)
    fun String?.isValidToken(): Boolean {
      contract { returns(true) implies (this@isValidToken != null) }
      return this != null && length == 32
    }
    …
  10. 10

    The compiler refuses to build until you add @OptIn(...). What is that mechanism, and when would you define your own marker?

    Medium

    Opt-in is a library author's way of marking an API as unstable or dangerous so that callers have to acknowledge it in writing before using it.

    @RequiresOptIn(
      message = "Sync API is experimental and may change without notice.",
      level = RequiresOptIn.Level.ERROR,
    )
    @Retention(AnnotationRetention.BINARY)
    annotation class ExperimentalSyncApi
    …
  11. 11

    A property typed String held null at runtime and crashed. How did a null get past the compiler?

    Medium

    Kotlin's non-null guarantee only covers code the Kotlin compiler checked, so nulls arrive from Java, from reflection and from assertions you wrote yourself.

    val label: String = javaLibrary.getTitle()     // platform type, no check
    val safeLabel = javaLibrary.getTitle() ?: "Untitled"   // defend at the boundary
    
    // Reflection-based JSON can produce an impossible object
    data class Profile(val id: Long, val name: String = "anon")
    val raw = readAsset("profile.json")        // the payload has no name field
    …
  12. 12

    In sealed interface UiState<out T>, why is data object Loading : UiState<Nothing> legal and useful?

    Medium

    Nothing has no instances at all and sits at the bottom of the type hierarchy, so with a covariant parameter UiState<Nothing> is a subtype of every UiState<T>.

    sealed interface UiState<out T> {
      data object Loading : UiState<Nothing>
      data class Success<T>(val data: T) : UiState<T>
      data class Error(val cause: Throwable) : UiState<Nothing>
    }
    …
  13. 13

    A map/filter/first chain over 50 000 rows is slow and allocation-heavy — what does asSequence() change, and when does it make things worse?

    Medium

    Collection operators are eager and each one allocates a full result list, so a three-step chain over 50 000 rows builds three 50 000-element lists; a sequence pulls one element through the whole chain at a time and allocates nothing in between.

    val rows: List<Row> = loadRows()            // 50_000 rows
    
    // Eager: two intermediate lists, expensive() runs 50_000 times
    val firstValid = rows
      .map { expensive(it) }                    // ArrayList #1, 50_000 elements
      .filter { it.isValid }                    // ArrayList #2
    …
  14. 14

    payload is List<String> refuses to compile, while payload as List<String> compiles with a warning and crashes three screens later — what is the compiler telling you?

    Medium

    The JVM erases generic arguments, so at runtime the object knows only that it is a List<String> exists in the compiler's head and nowhere else.

    val payload: Any = api.rawBody()
    
    // Does not compile — the argument is gone at runtime
    // if (payload is List<String>) { ... }
    
    // Compiles: "some list, argument unknown"
    …
  15. 15

    You wrote class LoggingRepo(inner: Repo) : Repo by inner and overrode add, but calls arriving through addAll log nothing — why?

    Medium

    Delegation is composition, not inheritance: the compiler generates a forwarding method per interface member that calls the wrapped object, and the wrapped object has no reference back to your class, so any self-call it makes stays inside itself.

    interface Repo {
      fun add(item: Item)
      fun addAll(items: List<Item>)
    }
    
    class DbRepo : Repo {
    …
  16. 16

    A lambda won't compile where your Kotlin listener interface is expected, and after you fix it removeListener { ... } removes nothing — what is going on?

    Medium

    SAM conversion applies only to Java interfaces and to Kotlin interfaces declared fun interface, and every conversion site creates a fresh object, so the lambda handed to remove is never the same instance as the one you added.

    // A plain Kotlin interface — a lambda will not convert
    interface OnEvent { fun onEvent(e: Event) }
    // player.addListener { }                   // type mismatch
    
    // One abstract method + `fun` → SAM conversion works
    fun interface OnEventListener {
    …
  17. 17

    You made a public library function inline and it stopped compiling against an internal helper, while old callers keep running the previous body — what did inlining change?

    Hard

    The body of an inline function is copied into every call site, so it becomes part of your published binary surface: it may only touch declarations the caller can see, and callers keep the copy they compiled against until they are rebuilt.

    // A public inline function cannot touch internal or private declarations
    internal fun buildDefaultClient(): HttpClient = HttpClient()
    // inline fun <reified T> request(path: String): T =
    //   buildDefaultClient().get(path, T::class)
    //   error: public-API inline function cannot access non-public-API 'buildDefaultClient'
    …
  18. 18

    Two properties in one class declared by shared end up holding the same value — what does by actually store, and how should a delegate know which property it serves?

    Hard

    by expr evaluates the expression once per property and stores the result in a hidden field, so two properties handed the same delegate object share every field that object holds; the only thing distinguishing the properties at call time is the KProperty passed into getValue/setValue.

    class Setting<T>(private val default: T) : ReadWriteProperty<Any?, T> {
      private var cached: T? = null                        // state lives in the DELEGATE
      override fun getValue(thisRef: Any?, property: KProperty<*>): T = cached ?: default
      override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { cached = value }
    }
    …
  19. 19

    Dagger says it cannot provide Set<Provider<Foo>> until you write Set<@JvmSuppressWildcards Provider<Foo>> — what is the compiler doing to your generic signature?

    Hard

    Kotlin translates declaration-site variance into Java wildcards when it writes the JVM signature, so a parameter typed Set<Provider<Foo>> is emitted as `Set<?

    // Kotlin declaration
    class Analytics @Inject constructor(
      private val trackers: Set<Tracker>,
    )
    // JVM signature Dagger sees: Analytics(Set<? extends Tracker>)
    // Dagger contributes Set<Tracker> → keys do not match
    …
  20. 20

    Your inline fun <reified T> fromJson(s: String): T returns a list of LinkedTreeMap for fromJson<List<User>>() — where exactly does reified stop?

    Hard

    reified captures the call site's class, not its type arguments: for T = List<User>, T::class is just List, so Gson is told to build a list and fills it with its own default LinkedTreeMaps, and the ClassCastException lands wherever you first read a field.

    // Broken: T::class is the raw class, the User argument is gone
    inline fun <reified T : Any> fromJson(json: String): T =
      gson.fromJson(json, T::class.java)
    
    val users: List<User> = fromJson(body)
    println(users[0].name)
    …