Kotlin Generics
Generics let you write code that works with any type instead of locking it to one specific type. A function or class can accept a type as a parameter — just as a function accepts a value as a parameter. This makes your code reusable and type-safe at the same time.
The Problem Generics Solve
// Without generics — you write the same box for every type: class IntBox(val value: Int) class StringBox(val value: String) class DoubleBox(val value: Double) // With generics — one class handles all types: class Box(val value: T) val intBox = Box(42) val stringBox = Box("Kotlin") val doubleBox = Box(3.14) println(intBox.value) // 42 println(stringBox.value) // Kotlin
Generic Functions
A generic function declares a type parameter before the function name using angle brackets.
funprintTwice(item: T) { println(item) println(item) } printTwice("Hello") // Hello \n Hello printTwice(100) // 100 \n 100 printTwice(true) // true \n true
Generic Function That Returns a Value
funfirstItem(list: List ): T { return list.first() } println(firstItem(listOf("A", "B", "C"))) // A println(firstItem(listOf(10, 20, 30))) // 10
Generic Classes
class Pair(val first: A, val second: B) { fun swap(): Pair = Pair(second, first) override fun toString() = "($first, $second)" } val coords = Pair(10, 20) println(coords) // (10, 20) println(coords.swap()) // (20, 10) val nameAge = Pair("Priya", 25) println(nameAge) // (Priya, 25)
Diagram — Generic Type Parameter Flow
Pair PairA ←────────── "Priya" (String) B ←────────── 25 (Int) first: A = "Priya" second: B = 25 swap() returns Pair = Pair
Type Constraints
You can restrict which types a generic can accept using an upper bound. The type parameter must be a subtype of the specified class or interface.
fun> findMax(a: T, b: T): T { return if (a > b) a else b } println(findMax(10, 20)) // 20 println(findMax("Kotlin", "Java")) // Kotlin (alphabetically) // findMax(object1, object2) // ERROR: object1 is not Comparable
Multiple Constraints with where
funprocess(item: T) where T : Comparable , T : Cloneable { // T must be both Comparable AND Cloneable }
Variance — in and out
Variance controls whether a generic type can be used as a supertype or subtype in a type hierarchy.
out (Covariance) — Producer
Use out when the class only produces values of type T — you can read but not write.
class Supplier(private val value: T) { fun get(): T = value } val intSupplier: Supplier = Supplier(42) val anySupplier: Supplier = intSupplier // OK because of 'out' println(anySupplier.get()) // 42
in (Contravariance) — Consumer
Use in when the class only consumes values of type T — you can write but not read.
class Printer{ fun print(item: T) { println(item) } } val anyPrinter: Printer = Printer() val stringPrinter: Printer = anyPrinter // OK because of 'in' stringPrinter.print("Hello") // Hello
Diagram — in vs out
out T (covariant) — Producer: Suppliercan be used as Supplier "Specific type → General type" is allowed Read-only in T (contravariant) — Consumer: Printer can be used as Printer "General type → Specific type" is allowed Write-only
Star Projection — * (Unknown Type)
Use * when you want to work with a generic collection but do not care what type it holds.
fun printSize(list: List<*>) {
println("Size: ${list.size}")
}
printSize(listOf(1, 2, 3)) // Size: 3
printSize(listOf("a", "b")) // Size: 2
printSize(listOf(true, false, true)) // Size: 3
Reified Type Parameters — Accessing T at Runtime
Generic types are normally erased at runtime (type erasure). The reified keyword — used only with inline functions — preserves the type information so you can use it at runtime.
inline funisType(value: Any): Boolean { return value is T } println(isType ("Hello")) // true println(isType ("Hello")) // false println(isType (42)) // true
