Irbisa · Cheatsheet

Kotlin Advanced

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

Middle Developer · 23 questions

  1. 01

    What is a typealias, what does it compile to, and why does it not make your code type-safe?

    A typealias is just a second name for an existing type: the compiler substitutes the real type everywhere, so nothing new appears in the bytecode and UserId stays fully interchangeable with Long.

    typealias UserId = Longtypealias ClickHandler = (position: Int) -> Unittypealias Cache<K> = MutableMap<K, ByteArray> fun ban(id: UserId) { /* ... */ }val orderId: Long = 42L…
  2. 02

    How does variance work in Kotlin generics (out, in, star projection), and how does it map to Java's PECS?

    Generic types are invariant by default; out T makes a type covariant because it only produces T, and in T makes it contravariant because it only consumes T.

    open class Animalclass Dog : Animal() interface Source<out T> { fun next(): T }        // producerinterface Sink<in T> { fun put(item: T) }        // consumer…
  3. 03

    Why does one Loading object fit every UiState<T> in this code, and what breaks if you remove out?

    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>} val users: UiState<List<User>> = UiState.Loadingval profile: UiState<User> = UiState.Loading

    Nothing is a subtype of every type, and with out T that subtyping carries over to the container, so UiState<Nothing> is a subtype of every UiState<T> and one Loading singleton serves all screens.

  4. 04

    What is type erasure, and why does the is check here not compile while the cast compiles and crashes later?

    fun handle(payload: Any) {    if (payload is List<String>) {           // compile error        println(payload.first().length)    }    val names = payload as List<String>      // warning: unchecked cast    println(names.first().length)            // ClassCastException here} handle(listOf(1, 2, 3))

    On the JVM generic arguments are erased: at runtime the object only knows it is a List, so is List<String> cannot be checked and is a compile error, while as List<String> checks only the List part and lets wrong elements through.

  5. 05

    What does inline do in Kotlin, what are noinline and crossinline for, and when is inline a bad idea?

    An inline function's body and its lambda arguments are copied into each call site, so no Function object is allocated and no extra call happens; noinline keeps one lambda as a real object, and crossinline forbids a non-local return from a lambda run in another context.

    inline fun <T> measure(label: String, block: () -> T): T {    val start = System.nanoTime()    return block().also { println("$label: ${System.nanoTime() - start} ns") }} fun firstAdult(users: List<User>): User? {…
  6. 06

    This inline helper does not compile: what is the compiler protecting you from, and what are the two ways to fix it?

    inline fun runOnUi(block: () -> Unit) {    Handler(Looper.getMainLooper()).post {        block()    }} fun save() {    runOnUi { if (isFinishing) return }}

    The lambda is inlined into save(), where a bare return means "return from save", but post runs it later inside a Runnable, when save has long returned, so the compiler rejects it: "Can't inline 'block' here: it may contain non-local returns".

  7. 07

    What is reified, why does it only work with inline functions, and can Java call such a function?

    reified makes a type parameter available at runtime: because the inline body is copied into each call site, the compiler substitutes the concrete type there, so T::class, is T and as T work.

    inline fun <reified T : Activity> Context.launch(vararg extras: Pair<String, Any?>) {    startActivity(Intent(this, T::class.java).putExtras(bundleOf(*extras)))} inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? =    BundleCompat.getParcelable(this, key, T::class.java)…
  8. 08

    How do property delegates work, and when would you use lazy, observable, vetoable, notNull or by map?

    val x by d hands the property's getter and setter to the object d: the compiler stores d in a hidden field and turns every read into d.getValue(this, ::x) and every write into d.setValue(this, ::x, value).

    class ProfileViewModel : ViewModel() {    private val formatter by lazy { DateTimeFormatter.ofPattern("dd.MM.yyyy") }     var name: String by Delegates.observable("") { _, old, new ->        Log.d("Profile", "name: $old -> $new")    }…
  9. 09

    Is by lazy thread-safe, and what do the LazyThreadSafetyMode options change?

    Yes by default: lazy { } uses SYNCHRONIZED, so the initializer runs exactly once even if several threads read the property at the same moment; the other two modes trade that guarantee for less locking.

    class ImageLoader(private val context: Context) {    // Default SYNCHRONIZED: safe to read from any thread    val client: OkHttpClient by lazy { buildOkHttpClient(context) }     // Read only on the main thread, so skip the lock    val placeholder: Drawable by lazy(LazyThreadSafetyMode.NONE) {…
  10. 10

    Find the bug: add() puts the item in the cart, but the screen never re-renders; only clear() updates it.

    class CartStore(private val onChange: (List<String>) -> Unit) {    var items: MutableList<String> by Delegates.observable(mutableListOf()) { _, _, new ->        onChange(new)    }        private set     fun add(item: String) {        items.add(item)    }     fun clear() {        items = mutableListOf()    }}

    Delegates.observable fires only when the property is reassigned, and items.add(item) mutates the same list through the getter, so the callback never runs and the screen misses every addition; clear() works only because it assigns a new list.

  11. 11

    What is a value class in Kotlin, and in which cases does it still allocate a wrapper object?

    @JvmInline value class UserId(val raw: Long) is a distinct type at compile time, but at runtime the compiler uses the underlying Long directly wherever it can, so you get type safety without an object per value.

    @JvmInlinevalue class UserId(val raw: Long) @JvmInlinevalue class Email(val value: String) {    init { require('@' in value) { "Invalid email: $value" } }…
  12. 12

    What do @JvmStatic, @JvmOverloads, @JvmField and @JvmName do for Java callers of your Kotlin code?

    They change the JVM shape of a Kotlin declaration so Java can call it naturally: a real static method, the overloads that default arguments imply, a plain field, or a different JVM name.

    @file:JvmName("StringUtils")package com.app.util fun slugify(s: String) = s.lowercase().replace(' ', '-')   // Java: StringUtils.slugify(...) class Config private constructor(val host: String) {…
  13. 13

    A property declared as non-null String held null at runtime and crashed: how did the null get past the compiler?

    Kotlin's null safety covers only code the Kotlin compiler checked; nulls slip in through Java platform types, reflection-based deserialization, !! and lateinit, unchecked generics and initialization order.

  14. 14

    How do annotations and code generation work in Kotlin: what does retention control, and why move from kapt to KSP?

    An annotation is metadata on a declaration; its retention decides where it survives (source only, the class file, or runtime reflection), and annotation processors read it at compile time to generate code, which is how Room, Hilt and Moshi codegen work.

    @Retention(AnnotationRetention.RUNTIME)@Target(AnnotationTarget.FUNCTION)annotation class Route(val path: String) // before: kapt(libs.room.compiler)// after:  ksp(libs.room.compiler)
  15. 15

    Why does the compiler demand @OptIn(...) before some API calls, and how do you declare an opt-in marker of your own?

    Opt-in marks an API as experimental or dangerous: its author annotates it with a marker, and every caller must acknowledge that marker in code, so a breaking change can only hit people who accepted the risk.

    @RequiresOptIn(    message = "Sync API is experimental and may change without notice.",    level = RequiresOptIn.Level.ERROR,)@Retention(AnnotationRetention.BINARY)@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)…
  16. 16

    You made a public library function inline: it no longer compiles against an internal helper, and old callers keep running the previous body. Why?

    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 were compiled against until they are rebuilt.

    internal val defaultJson = Json { ignoreUnknownKeys = true } // error: Public-API inline function cannot access non-public-API 'defaultJson'// inline fun <reified T> decode(text: String): T = defaultJson.decodeFromString(text) @PublishedApi…
  17. 17

    Find the bug: after a while, every other thread that calls refresh() blocks on this lock forever.

    inline fun <T> ReentrantLock.guarded(block: () -> T): T {    lock()    return block().also { unlock() }} class TokenCache(private val api: Api) {    private val lock = ReentrantLock()    private var token: String? = null     fun refresh() {        lock.guarded {            if (token != null) return // already fresh            token = api.fetchToken()        }    }}

    guarded unlocks only after block returns normally.

  18. 18

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

    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 default LinkedTreeMaps, and the ClassCastException lands wherever you first read a field.

    // wrong: T::class is the raw class, the User argument is goneinline fun <reified T : Any> fromJson(json: String): T =    gson.fromJson(json, T::class.java) val users: List<User> = fromJson(body)println(users[0].name)   // ClassCastException: LinkedTreeMap cannot be cast to User…
  19. 19

    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?

    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 that tells them apart at call time is the KProperty passed to 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 }}…
  20. 20

    How do you build a type-safe DSL in Kotlin, and what does @DslMarker protect you from?

    A Kotlin DSL is ordinary code built on lambdas with receiver: a function takes block: Builder.() -> Unit, creates the builder, runs the block with the builder as this, and returns the result, while @DslMarker stops nested blocks from silently calling an outer builder.

    @DslMarkerannotation class MenuDsl data class Item(val title: String, val icon: Int?, val onClick: () -> Unit)data class Menu(val items: List<Item>)…
  21. 21

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

    The standard library function declares a contract (a promise to the compiler about what its return value implies about its arguments), while your helper is an opaque call, so the compiler cannot know that false means "not null" and drops the smart cast.

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

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

    A context parameter is a dependency a function declares but nobody passes explicitly: the compiler fills it from a matching context already in scope, so ambient things like a logger or a transaction stop being threaded through every signature.

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

    What changed with the K2 compiler and Kotlin 2.x, and what does it mean for an Android project?

    Kotlin 2.0 made K2, a rewritten compiler frontend, the default: faster analysis and smarter smart casts with almost no source changes, and the 2.x releases since then use it to ship new language features on a regular cadence.

    sealed interface Response {    data class Ok(val body: String) : Response    data class Failure(val code: Int, val retryable: Boolean) : Response} // Guard condition in when (stable in 2.2)…