Kotlin Advanced
Generics, inline/reified, delegates, DSL builders, type aliases
- 01
How do generics, variance, and
out/inmodifiers work in Kotlin?MediumKotlin declares variance at the declaration site:
outmarks a type parameter the class only produces,inone 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 … - 02
What are inline functions and reified type parameters in Kotlin?
MediumAn
inline funis 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") } … - 03
What are property delegates? Explain
by lazy,by Delegates.observable, custom delegates.MediumProperty delegation hands a property's get and set to another object:
byforwards every read and write to that object'sgetValue/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 … - 04
How do you write a type-safe DSL in Kotlin?
HardA type-safe Kotlin DSL rests on three features: lambdas with receiver, extension functions and the
@DslMarkerannotation.@DslMarker annotation class HtmlDsl @HtmlDsl class Html { val children = mutableListOf<Tag>() } … - 05
What are typealiases and when should you use them?
Mediumtypealiasintroduces a name for an existing type.typealias UserId = Long typealias Cache = MutableMap<String, ByteArray> typealias EventHandler = (Event) -> Unit class Authentication { fun login(): UserId = 42L } … - 06
What are value classes (inline classes) in Kotlin?
HardA 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 … - 07
Your Kotlin module is consumed from Java. Which declarations need a
@Jvm...annotation to look right on the other side?MediumCompanion objects, default arguments, top-level functions and property annotations all compile into shapes a Java caller finds awkward, and the
@Jvmfamily 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") … - 08
What are context parameters in Kotlin, and what problem do they solve that an extra function argument does not?
HardA 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") … - 09
Why does a smart cast survive
if (!s.isNullOrEmpty())but disappear through an identical helper you wrote yourself?HardThe 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
The compiler refuses to build until you add
@OptIn(...). What is that mechanism, and when would you define your own marker?MediumOpt-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
A property typed
Stringheld null at runtime and crashed. How did a null get past the compiler?MediumKotlin'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
In
sealed interface UiState<out T>, why isdata object Loading : UiState<Nothing>legal and useful?MediumNothinghas no instances at all and sits at the bottom of the type hierarchy, so with a covariant parameterUiState<Nothing>is a subtype of everyUiState<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
A
map/filter/firstchain over 50 000 rows is slow and allocation-heavy — what doesasSequence()change, and when does it make things worse?MediumCollection 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
payload is List<String>refuses to compile, whilepayload as List<String>compiles with a warning and crashes three screens later — what is the compiler telling you?MediumThe 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
You wrote
class LoggingRepo(inner: Repo) : Repo by innerand overrodeadd, but calls arriving throughaddAlllog nothing — why?MediumDelegation 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
A lambda won't compile where your Kotlin listener interface is expected, and after you fix it
removeListener { ... }removes nothing — what is going on?MediumSAM 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 toremoveis 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
You made a public library function
inlineand it stopped compiling against an internal helper, while old callers keep running the previous body — what did inlining change?HardThe 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
Two properties in one class declared
by sharedend up holding the same value — what doesbyactually store, and how should a delegate know which property it serves?Hardby exprevaluates 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 theKPropertypassed intogetValue/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
Dagger says it cannot provide
Set<Provider<Foo>>until you writeSet<@JvmSuppressWildcards Provider<Foo>>— what is the compiler doing to your generic signature?HardKotlin 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
Your
inline fun <reified T> fromJson(s: String): Treturns a list ofLinkedTreeMapforfromJson<List<User>>()— where exactly doesreifiedstop?Hardreifiedcaptures the call site's class, not its type arguments: forT = List<User>,T::classis justList, so Gson is told to build a list and fills it with its own defaultLinkedTreeMaps, 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) …