Kotlin Generics
Generics let you write code that works with any type, while still keeping type safety. Instead of writing separate functions or classes for Int, String, Double, and so on, you write one generic version that works for all of them.
The Problem Generics Solve
Without generics — repetitive:
fun printInt(value: Int) = println(value)
fun printString(value: String) = println(value)
fun printDouble(value: Double) = println(value)
// One function per type — not scalable
With generics — one function, any type:
fun print(value: T) = println(value)
print(42) // works
print("Hello") // works
print(3.14) // works
Generic Functions
fun firstOrNull(list: List): T? = if (list.isEmpty()) null else list[0]
fun swap(pair: Pair): Pair = Pair(pair.second, pair.first)
fun main() {
println(firstOrNull(listOf(10, 20, 30))) // 10
println(firstOrNull(listOf())) // null
println(firstOrNull(listOf("a", "b"))) // a
println(swap(Pair(1, 2))) // (2, 1)
println(swap(Pair("Hi", "Bye"))) // (Bye, Hi)
} Generic Classes
class Box(val content: T) {
fun describe() = "Box containing: $content (${content!!::class.simpleName})"
fun unwrap(): T = content
}
fun main() {
val intBox = Box(42)
val stringBox = Box("Kotlin")
val listBox = Box(listOf(1, 2, 3))
println(intBox.describe()) // Box containing: 42 (Int)
println(stringBox.describe()) // Box containing: Kotlin (String)
println(listBox.unwrap()) // [1, 2, 3]
} Multiple Type Parameters
class KeyValue(val key: K, val value: V) {
override fun toString() = "$key → $value"
}
fun mapOf2(k1: K, v1: V, k2: K, v2: V) = listOf(
KeyValue(k1, v1),
KeyValue(k2, v2)
)
fun main() {
val entry = KeyValue("userId", 12345)
println(entry) // userId → 12345
val table = mapOf2("a", 1, "b", 2)
table.forEach { println(it) }
} Type Constraints
Use : UpperBound to restrict which types can be used with a generic:
// T must be a Number (Int, Double, Long, etc.)
fun sum(a: T, b: T): Double = a.toDouble() + b.toDouble()
// T must implement Comparable
fun > largest(a: T, b: T): T = if (a > b) a else b
fun main() {
println(sum(10, 20)) // 30.0
println(sum(3.5, 1.5)) // 5.0
println(largest(100, 200)) // 200
println(largest("apple", "mango")) // mango (alphabetically greater)
} Generic Interface
interface Repository {
fun save(item: T)
fun findById(id: Int): T?
fun findAll(): List
}
data class Product(val id: Int, val name: String)
class ProductRepository : Repository {
private val store = mutableMapOf()
override fun save(item: Product) { store[item.id] = item }
override fun findById(id: Int): Product? = store[id]
override fun findAll(): List = store.values.toList()
}
fun main() {
val repo = ProductRepository()
repo.save(Product(1, "Laptop"))
repo.save(Product(2, "Mouse"))
println(repo.findById(1)) // Product(id=1, name=Laptop)
println(repo.findAll())
} Practical Example: Generic Stack
class Stack {
private val items = mutableListOf()
fun push(item: T) { items.add(item) }
fun pop(): T? = if (items.isEmpty()) null else items.removeAt(items.lastIndex)
fun peek(): T? = items.lastOrNull()
val size: Int get() = items.size
val isEmpty: Boolean get() = items.isEmpty()
override fun toString() = items.toString()
}
fun main() {
val stack = Stack()
stack.push("First")
stack.push("Second")
stack.push("Third")
println(stack) // [First, Second, Third]
println(stack.pop()) // Third
println(stack.peek()) // Second
println(stack.size) // 2
} 