iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Generics

Kotlin generics: type parameters, variance (in / out), reified types, and the patterns that age well.

Kotlin — generics

EXAMPLE
// ===== Basic generic class =====
class Box<T>(val value: T) {
    fun isEmpty() = value == null
}

val b1 = Box("hello")
val b2 = Box(42)

// ===== Generic function =====
fun <T> first(xs: List<T>): T? = xs.firstOrNull()

val s = first(listOf("a", "b"))      // String?
val n = first(listOf(1, 2))           // Int?

// ===== Constraints =====
fun <T : Comparable<T>> max(a: T, b: T): T = if (a >= b) a else b

fun <T> sortAndShow(xs: List<T>) where T : Comparable<T>, T : Any {
    println(xs.sorted())
}

// ===== Variance =====
// Default: invariant. Box<String> is NOT a Box<Any>.

// 'out': covariant — read-only producer
interface Producer<out T> {
    fun produce(): T
}

val sp: Producer<Any> = (object : Producer<String> { override fun produce() = "hello" })
// Producer<String> can be used as Producer<Any> because produces String which is Any.

// 'in': contravariant — write-only consumer
interface Consumer<in T> {
    fun consume(x: T)
}

val ca: Consumer<String> = (object : Consumer<Any> { override fun consume(x: Any) {} })
// Consumer<Any> can be used as Consumer<String> because consumes Any which a String is.

// Mnemonic: 'producers out, consumers in' (PECS).

// ===== Star projection =====
fun printAll(xs: List<*>) {
    for (x in xs) println(x)
}
// '*' = some type, we don't know what. Read-only safe; write not.

// ===== Reified types (inline functions only) =====
inline fun <reified T> Any.castOrNull(): T? = this as? T

val maybeStr: String? = anyValue.castOrNull()

// reified lets you reference T at runtime (Class.forName style).
// Only works inside 'inline' functions.

// ===== Type-safe builder pattern =====
class HtmlBuilder {
    private val children = mutableListOf<String>()
    fun p(text: String) { children.add("<p>$text</p>") }
    fun build() = children.joinToString("")
}

fun html(block: HtmlBuilder.() -> Unit): String {
    val b = HtmlBuilder()
    b.block()
    return b.build()
}

val output = html {
    p("Hello")
    p("World")
}

// ===== When variance bites =====
val intList: List<Int> = listOf(1, 2, 3)
val anyList: List<Any> = intList     // List is OUT (covariant); ok
// val mutableInt: MutableList<Int> = mutableListOf()
// val mutableAny: MutableList<Any> = mutableInt   // ERROR: MutableList is invariant

// ===== Patterns to internalise =====
// - Default to invariant; opt into out / in deliberately
// - Reified for type-driven APIs
// - Constraints via where for multi-bound type parameters
// - Star projection for read-only iteration where T does not matter

// ===== Pitfalls =====
// - Variance via casting (forced) -> runtime errors
// - reified outside an inline function -> compile error
// - Generic Result<T> with bare Any -> erases the typing benefit
// - Confusion: 'out T' means PRODUCING T; 'in T' means CONSUMING T

Why it matters

Kotlin generics: invariant by default, opt into out (covariant) or in (contravariant) for producers/consumers, reified for runtime-aware APIs in inline functions, where for multiple constraints. Master PECS and the type system stops being magic.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
fun <T : Comparable<T>> largest(xs: List<T>): T = xs.max()
println(largest(listOf(3, 1, 4, 1, 5)))
Try it Yourself »

Discussion

Loading…