Lesson 5 of 83 intermediate

Generics, Variance, Extension Functions & Inline/Reified

Advanced Kotlin type system mastery for senior-level interviews

Open interactive version (quiz + challenge)

Real-world analogy

Generics are like a universal adapter — one design that works with any plug type. Variance is the rule about whether a USA-to-EU adapter can be substituted for a USA-to-UK adapter. Extension functions are like adding a new tool to someone else's toolbox without opening it.

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

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. 1. sealed class ApiResult — out makes it covariant; ApiResult is usable where ApiResult is expected
  2. 2. ApiResult() for Error and Loading — Nothing is subtype of all types; covariant Nothing works as any ApiResult
  3. 3. fun > List.minMax() — generic extension with upper bound; T must be comparable to call min()/max()
  4. 4. fun Response.toApiResult() — extension on Retrofit Response; transforms network response to domain result type
  5. 5. body()?.let { ApiResult.Success(it) } ?: ApiResult.Error(...) — safe call chain; null body becomes Error, not NPE
  6. 6. inline fun String.parseJson() — inline required for reified; T::class.java accessible at runtime
  7. 7. Gson().fromJson(this, T::class.java) — 'this' is the String; T::class.java is the runtime class thanks to reified
  8. 8. fun T.visibleIf(): T — returns T (not View), so button.visibleIf(true).setOnClickListener{} works without cast
  9. 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?
MutableList is invariant (unlike List). The parser call needs explicit type specification.
Show answer
Bug 1: MutableList<Dog> is NOT a subtype of MutableList<Animal> — MutableList is invariant because you could add a Cat to an Animal list, violating type safety. Fix: return List<Animal> (read-only covariant) or use List<Dog> if Dog is what you need. Bug 2: parseResponse(userJson) — the compiler cannot infer T without a type hint. Fix: val user = parseResponse<User>(userJson) or val user: User = parseResponse(userJson).

Explain like I'm 5

Generics are like a magic backpack that can hold ANY type of item, but only one type at a time — you tell it 'this backpack is for books' and it only holds books. Variance is the rule about whether a book-backpack can be used as a thing-backpack. Extension functions are like sticking a new pocket onto someone else's jacket without sewing — it looks like it was always there!

Fun fact

Kotlin's extension functions are resolved at compile time, not runtime — this means they don't participate in virtual dispatch. If you have a val x: Animal = Dog() and call x.speak() where speak() is an extension on Animal and Dog, the Animal version is called. This surprises many Java developers who expect polymorphic behavior.

Hands-on challenge

Build a type-safe local cache for an offline-first field ops app. Create a generic class Cache with get(key: String): T?, put(key: String, value: T), and remove(key: String). Add an inline reified extension fun Cache.getTyped(key: String): T? that safely retrieves and casts. Add a generic extension fun Cache.evictExpired() using a where bound. Write a fun Cache.getOrFetch(key: String, fetcher: () -> T): T.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery