Skip to content
AgileLogics
6 min read

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.