Generics
Prerequisites: Interfaces
Generics let you write one function or class that works with many
types while keeping full type safety. List<String> and
Map<String, Int> are generic types you've used all along.
A generic function
fun <T> firstAndLast(items: List<T>): Pair<T, T>? {
if (items.isEmpty()) return null
return items.first() to items.last()
}
fun main() {
println(firstAndLast(listOf(3, 7, 9)))
println(firstAndLast(listOf("a", "b")))
println(firstAndLast(emptyList<Int>()))
}
(3, 9)
(a, b)
null
<T> introduces a type parameter — a placeholder filled in at each
call. Unlike using Any, the result keeps its real type.
Constraints
A constraint requires the type to have certain abilities:
fun <T : Comparable<T>> largest(items: List<T>): T? {
var best: T? = null
for (item in items) {
if (best == null || item > best) best = item
}
return best
}
fun main() {
println(largest(listOf(4, 9, 2)))
println(largest(listOf("pear", "apple", "zucchini")))
}
9
zucchini
Without T : Comparable<T>, the > wouldn't compile — not every type
can be compared.
A generic class
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) = items.add(item)
fun pop(): T? = items.removeLastOrNull()
fun peek(): T? = items.lastOrNull()
val isEmpty get() = items.isEmpty()
}
fun main() {
val history = Stack<String>()
history.push("home")
history.push("settings")
println(history.pop())
println(history.peek())
}
settings
home
Variance in brief: out and in
A List<String> can be used where a List<Any> is expected, because
List is declared List<out E> — it only ever produces values of
E. A MutableList<String> can't, because a MutableList<Any> would
let someone add a number to your list of strings. Level 3 covers
variance in depth.
Common mistakes
- Making code generic before it needs to be. Write the concrete version first; generalize when a second type actually needs it.