Phase 4 · Collections & GenericsModule 22~38 min read

Generics

Write type-safe, reusable code with generics and Kotlin's variance (in/out).

What you'll learn

Generics let you write code that works with any type while staying fully type-safe — the reason List<String> knows it holds strings. This module covers generic classes and functions, constraints, and Kotlin's clean approach to variance.

By the end you'll be able to:

  • Write generic classes and functions
  • Constrain type parameters
  • Understand variance with out and in

Generic classes & functions

A generic type takes a type parameter in angle brackets — conventionally T. Inside, T stands for whatever type the caller uses, and the compiler infers it from the arguments:

Box.kt
class Box<T>(val item: T) {   // T is a type parameter
    fun get(): T = item
}

val stringBox = Box("Hello")   // T inferred as String
val intBox = Box(42)           // T inferred as Int

println(stringBox.get())   // Hello
println(intBox.get())      // 42

Constraints (bounded types)

Sometimes T can't be anything — you need it to have certain capabilities. An upper bound like <T : Comparable<T>> restricts T to types that satisfy it, so you can use those capabilities (here, comparing with >):

Constraint.kt
// <T : Comparable<T>> constrains T to types that can be compared
fun <T : Comparable<T>> larger(a: T, b: T): T =
    if (a > b) a else b

println(larger(3, 7))            // 7
println(larger("apple", "pear")) // pear

Variance: out & in

Here's a subtle question: should a List<Cat> be usable where a List<Animal> is expected? Intuitively yes — every cat is an animal. Kotlin controls this with variance markers on the type parameter:

Producers and consumers

out T — producer

You only read T out (like a List). Lets a List<Cat> be used where a List<Animal> is expected (covariance).

in T — consumer

You only write T in (like a comparator). The reverse relationship (contravariance).

Variance.kt
open class Animal(val name: String)
class Cat(name: String) : Animal(name)

// List<out T> is covariant, so a List<Cat> works where List<Animal> is expected
fun printNames(animals: List<Animal>) {
    animals.forEach { println(it.name) }
}

val cats: List<Cat> = listOf(Cat("Milo"), Cat("Felix"))
printNames(cats)   // works!

Producer out, consumer in

The mnemonic: use out when a type only produces values you read (safe to be covariant), and in when it only consumes values you write. Kotlin's declaration-site variance (you mark it once on the class) is far cleaner than Java's wildcards scattered at every use.

Note

Like Java, Kotlin generics are erased at runtime — the type argument isn't available then. That's why you can't write value is T normally; the reified trick from Module 12 (inside an inline function) is the workaround. The star projection <*> means "some type, but I don't know or care which."

Recap & quick check

Key takeaways

  • Generics provide type-safe, reusable code; a type parameter <T> is filled in by the caller.
  • Generic classes and functions both use type parameters; the compiler usually infers them.
  • An upper bound like <T : Comparable<T>> constrains T so you can use those capabilities.
  • Variance: 'out' (producer, covariant — read only) and 'in' (consumer, contravariant — write only).
  • Kotlin uses declaration-site variance (marked once on the class), cleaner than Java's wildcards; generics are erased at runtime.

Quick check

1. What is the main benefit of generics?

2. What does <T : Comparable<T>> do?

3. What does the 'out' variance marker mean?

4. How does Kotlin's variance differ from Java's?

5. Are Kotlin generics available at runtime?

Excellent — generics unlock type-safe, reusable code. Next up: Module 23 — Delegation & Delegated Properties, the finale of Phase 4.