Generics, Variance, Extension Functions & Inline/Reified
Advanced Kotlin type system mastery for senior-level interviews
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Generics enable type-safe reusable code. Variance (in/out) controls type substitutability — critical for understanding Kotlin's collection API design. Extension functions add behavior to existing classes without modification — the backbone of idiomatic Kotlin Android code. Inline + reified solves the JVM's type erasure problem for generic functions.
Real-world relevance
In a fintech app's networking layer, a generic ApiResult sealed class with out variance allows a function returning ApiResult to be stored in a variable of type ApiResult. Extension functions like Response.toApiResult() clean up Retrofit response handling. An inline reified function parseResponse() eliminates class parameter boilerplate in every API call.
Key points
- Generic classes and functions — fun wrapInList(item: T): List = listOf(item). T is a type parameter — the compiler infers it at call sites. Generic constraints: fun > max(a: T, b: T): T — T must implement Comparable.
- where clause for multiple bounds — fun process(item: T) where T : Serializable, T : Comparable — T must satisfy both bounds. Use when a single upper bound isn't sufficient. Common in repository layers with generic entity constraints.
- Covariance with 'out' (producer) — class Box(val value: T) — out means Box is a subtype of Box. T can only appear in 'out' positions (return types). Makes sense for producers: List produces T, never consumes it.
- Contravariance with 'in' (consumer) — class Comparator — in means Comparator is a subtype of Comparator. T can only appear in 'in' positions (function parameters). Makes sense for consumers: a function that accepts Animal can accept Dog.
- Star projection — List is like List — you can read from it (getting Any?) but cannot write to it (type unknown). Use when you need to handle a generic type but don't know or care about the exact type parameter.
- Extension functions — fun String.toSlug(): String = lowercase().replace(' ', '-') adds a method to String without subclassing. Resolved statically at compile time (not virtual dispatch). Defined in a file, scoped to where they're imported. Cannot access private members.
- Extension functions for clean APIs — fun Context.showToast(msg: String) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() eliminates verbose Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() everywhere. Common pattern in Android for View, Context, Fragment extensions.
- inline keyword — inline fun measureTime(block: () -> T): T inlines the lambda body at the call site — no Function object created, no virtual dispatch. Critical for performance in hot paths. Required for reified type parameters.
- reified type parameters — inline fun Gson.fromJson(json: String): T = fromJson(json, T::class.java) — reified allows T to be accessed at runtime as an actual class. Only possible with inline functions. Eliminates class: Class parameters.
- noinline and crossinline — noinline fun param: () -> Unit prevents a specific lambda from being inlined (when you need to store it). crossinline fun param: () -> Unit prevents non-local returns from the lambda (when lambda runs in a different execution context).
- Generic extension functions — fun T.visibleIf(condition: Boolean): T { visibility = if (condition) View.VISIBLE else View.GONE; return this } — generic extension preserves the concrete type for chaining. Used in view builder DSLs.
- Type erasure and its limits — At runtime, List and List are both just List — type parameters are erased by the JVM. This is why is List doesn't compile (Unchecked cast warning). reified + inline is Kotlin's workaround for accessing erased types at runtime.
Code example
// Generic sealed result with covariance
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
object Loading : ApiResult<Nothing>()
}
// Generic function with constraint
fun <T : Comparable<T>> List<T>.minMax(): Pair<T, T>? {
if (isEmpty()) return null
return Pair(min(), max())
}
// Extension function on Response — clean API layer
fun <T> Response<T>.toApiResult(): ApiResult<T> = when {
isSuccessful -> body()?.let { ApiResult.Success(it) }
?: ApiResult.Error(204, "Empty response body")
else -> ApiResult.Error(code(), message())
}
// inline + reified — eliminates Class<T> parameter
inline fun <reified T> String.parseJson(): T =
Gson().fromJson(this, T::class.java)
// Usage — clean, no class param needed
val user: User = jsonString.parseJson()
val orders: List<Order> = ordersJson.parseJson()
// Generic View extension preserving concrete type
fun <T : View> T.visibleIf(condition: Boolean): T {
visibility = if (condition) View.VISIBLE else View.GONE
return this
}
// Star projection — handle unknown generic type
fun logListContents(list: List<*>) {
list.forEach { element -> println(element) } // element is Any?
}
// Multiple bounds with where
fun <T> syncToServer(item: T): Boolean
where T : Serializable, T : HasId {
return api.upload(item.id, item)
}Line-by-line walkthrough
- 1. sealed class ApiResult — out makes it covariant; ApiResult is usable where ApiResult is expected
- 2. ApiResult() for Error and Loading — Nothing is subtype of all types; covariant Nothing works as any ApiResult
- 3. fun > List.minMax() — generic extension with upper bound; T must be comparable to call min()/max()
- 4. fun Response.toApiResult() — extension on Retrofit Response; transforms network response to domain result type
- 5. body()?.let { ApiResult.Success(it) } ?: ApiResult.Error(...) — safe call chain; null body becomes Error, not NPE
- 6. inline fun String.parseJson() — inline required for reified; T::class.java accessible at runtime
- 7. Gson().fromJson(this, T::class.java) — 'this' is the String; T::class.java is the runtime class thanks to reified
- 8. fun T.visibleIf(): T — returns T (not View), so button.visibleIf(true).setOnClickListener{} works without cast
- 9. fun logListContents(list: List) — star projection; can iterate (reads as Any?), cannot add elements
Spot the bug
class Container<T>(val value: T)
fun fillContainers(animals: MutableList<Animal>): MutableList<Animal> {
val dogs: MutableList<Dog> = mutableListOf(Dog("Rex"))
return dogs // type mismatch
}
inline fun <reified T> parseResponse(json: String): T {
return Gson().fromJson(json, T::class.java)
}
fun useParser() {
val user = parseResponse(userJson) // missing type hint
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Generics in Kotlin (kotlinlang.org)
- Extension Functions (kotlinlang.org)
- Inline Functions and Reified (kotlinlang.org)
- Kotlin Type System — Variance (kotlinlang.org)