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…