Generics
Reviewed & published by Brayan K
Kotlin is a modern, concise language whose generics let you write one class or function that works safely with any type — the same machinery behind List, Map, and Set.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write generic classes and functions, add type constraints, and understand variance at a high level.
What You'll Learn in This Lesson
- What generics are and why they're type-safe
- Writing generic classes with <T>
- Writing generic functions
- Multiple type parameters <K, V>
- Constraining types with upper bounds
- A high-level look at in/out variance
1️⃣ Generic Classes and Functions
A type parameter in angle brackets, like <T>, is a placeholder for a real type supplied when the generic is used. The compiler tracks it, so a Box<Int> returns an Int — no casts needed.
// A generic class: <T> is a type PLACEHOLDER filled in per use.
class Box<T>(val item: T) {
fun get(): T = item
}
// A generic function infers T from its argument.
fun <T> firstOf(list: List<T>): T = list[0]
// Multiple type parameters:
fun <K, V> pairUp(key: K, value: V): Pair<K, V> = Pair(key, value)
fun main() {
val intBox = Box(42) // T = Int
val strBox = Box("hello") // T = String
println(intBox.get()) // -> 42
println(strBox.get()) // -> hello
println(firstOf(listOf("a", "b"))) // -> a
println(firstOf(listOf(10, 20))) // -> 10
val p = pairUp("age", 36) // Pair<String, Int>
println(p) // -> (age, 36)
}Notice the compiler inferred T from the arguments — you rarely have to write the type explicitly.
2️⃣ Constraints and a Generic Stack
An upper bound like <T : Comparable<T>> restricts T to types you can compare, unlocking operators like >. A generic Stack<T> shows how one reusable type works for strings, numbers, or anything else.
// Constrain T with an upper bound: T must be Comparable<T>.
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b
// A reusable generic stack:
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) { items.add(item) }
fun pop(): T = items.removeAt(items.size - 1)
fun isEmpty(): Boolean = items.isEmpty()
val size: Int get() = items.size
}
fun main() {
println(maxOf(3, 9)) // -> 9
println(maxOf("apple", "pear")) // -> pear (alphabetical)
val stack = Stack<String>()
stack.push("first")
stack.push("second")
println("Size: ${stack.size}") // -> Size: 2
println(stack.pop()) // -> second (last in, first out)
println(stack.pop()) // -> first
println("Empty: ${stack.isEmpty()}") // -> Empty: true
}The same maxOf compared both numbers and strings, because both are Comparable. Write it once, reuse it everywhere.
Your turn. Replace the TODO, then run and compare.
// 🎯 YOUR TURN — replace each TODO then run main()
// 1) A generic Pair-like holder. Fill in the type placeholder.
class Holder<TODO>(val value: T) { // 👉 replace TODO with 'T'
fun reveal(): T = value
}
// 2) A generic function that wraps a value in a single-item list.
fun <T> wrap(item: T): List<T> = listOf(item)
fun main() {
val h = Holder(99)
println(h.reveal()) // expect 99
println(wrap("solo")) // expect [solo]
}Mini-Challenge: A Generic Pair Printer
Write generic functions with one and two type parameters and call them with different types.
// 🎯 MINI-CHALLENGE: a generic Pair printer.
// Save as Generics.kt and run main().
//
// 1. Write fun <A, B> show(a: A, b: B): String that returns
// "(<a>, <b>)".
// 2. Write a generic fun <T> lastOf(list: List<T>): T that returns
// the final element (list[list.size - 1]).
// 3. In main(), print show("x", 10) and lastOf(listOf(1, 2, 3)).
//
// ✅ Example output:
// (x, 10)
// 3
fun main() {
// your code here
}Common Errors (and the fix)
- Forgetting <T> before a generic function — Declare the parameter: fun <T> wrap(x: T), not just fun wrap(x: T).
- "Operator '>' cannot be applied to T" — Add an upper bound <T : Comparable<T>> so comparison is allowed.
- Using Any instead of a type parameter — That loses type safety and forces casts. Prefer a generic <T>.
- Type can't be inferred — When the compiler can't work out T, supply it explicitly: Box<String>("hi").
Pro Tips
- 💡 Lean on inference: let the compiler deduce T from arguments and only annotate when it can't.
- 💡 Name parameters meaningfully: K/V for maps, E for elements, T generally.
- 💡 reified types: with inline functions you can use reified to access T at runtime (advanced).
📋 Quick Reference — Generics
| Concept | Kotlin Syntax |
|---|---|
| Generic class | class Box<T>(val item: T) |
| Generic function | fun <T> firstOf(l: List<T>): T |
| Two params | fun <K, V> pairUp(k: K, v: V) |
| Upper bound | <T : Comparable<T>> |
| Explicit type | Box<String>("hi") |
🎉 Lesson Complete!
- ✅ Generics write one type-safe class/function for many types
- ✅ <T> is a placeholder filled in per use
- ✅ Functions infer T from arguments
- ✅ Upper bounds like <T : Comparable<T>> add capabilities
- ✅ Variance (in/out) makes library types flexible
Practice quiz
What does a type parameter like <T> represent?
- A fixed type that is always Int
- A placeholder for a real type supplied per use
- A runtime null value
- A reserved keyword for objects
Answer: A placeholder for a real type supplied per use. <T> is a placeholder filled in with a real type when the generic is used.
How is a generic function declared in Kotlin?
- fun firstOf<T>(...)
- fun firstOf(...)<T>
- fun <T> firstOf(...)
- generic fun firstOf(...)
Answer: fun <T> firstOf(...). The type parameter comes before the function name: fun <T> firstOf(...).
In Box(42), how does the compiler know T is Int?
- By type inference from the argument
- It defaults to Any
- You must always write Box<Int>
- It guesses randomly
Answer: By type inference from the argument. The compiler infers T = Int from the constructor argument 42.
What does the upper bound <T : Comparable<T>> guarantee?
- T is always a String
- T can be null
- Values of T can be compared with < and >
- T has no methods
Answer: Values of T can be compared with < and >. The bound guarantees T implements Comparable, so comparison operators work.
Which naming convention is typically used for a map's key and value parameters?
- X and Y
- A and B
- T and E
- K and V
Answer: K and V. K and V are the conventional names for key and value type parameters.
What is the main benefit of generics over using Any?
- They run faster at startup
- They preserve compile-time type safety without casts
- They allow null everywhere
- They remove the need for functions
Answer: They preserve compile-time type safety without casts. Generics keep full type safety, so no casting is needed unlike with Any.
How many type parameters can a generic function declare?
- Exactly one
- At most two
- One or more, such as <K, V>
- None
Answer: One or more, such as <K, V>. A function can declare multiple type parameters, e.g. fun <K, V> pairUp(...).
Why is Kotlin's List declared as List<out E>?
- To make it mutable
- To make it covariant (a producer)
- To forbid reading elements
- To require explicit casts
Answer: To make it covariant (a producer). out makes List covariant, so a List<String> works where List<Any> is wanted.
When the compiler can't infer T, how do you supply it?
- Use Any instead
- Cast the result
- Add a semicolon
- Provide it explicitly, like Box<String>("hi")
Answer: Provide it explicitly, like Box<String>("hi"). Supply the type argument explicitly when inference fails.
What does 'in T' (contravariance) mark a type parameter as?
- A consumer position
- A producer position
- An invariant position
- A nullable position
Answer: A consumer position. in marks a consumer position — the type only accepts T as input.
Continue this course
- Previous: Sealed Classes & Enums
- Next: Extension Functions — Add new functions to existing types without inheritance
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What problem do generics solve?
Generics let you write a single class or function that works with many types while keeping full type safety. Without them you'd either duplicate code for each type (a StringBox, an IntBox, …) or fall back to a loose type like Any and lose compile-time checks. With Box<T>, the compiler knows a Box<String> holds Strings, so box.get() returns a String — no casting, no runtime surprises. Kotlin's own List<T>, Map<K, V>, and Set<T> are all generic.
What is a type parameter and how does T get its value?
A type parameter, written in angle brackets like <T>, is a placeholder for a real type that is supplied when the generic is used. For a class you often specify it explicitly (Box<String>("hi")) or let it be inferred from the constructor argument (Box(42) infers Int). For a generic function, the compiler usually infers the type from the arguments you pass, so you rarely write it by hand. The names T, E, K, V are conventions for 'type', 'element', 'key', and 'value'.
What does an upper bound like <T : Comparable<T>> mean?
An upper bound constrains which types may be used for T. Writing <T : Comparable<T>> says 'T can be any type, as long as it implements Comparable<T>', which guarantees you can use the < and > operators on values of type T inside the function. Bounds let generic code rely on certain capabilities — sorting, comparing, or calling specific methods — while still being reusable across all types that meet the requirement.
What are 'in' and 'out' (variance) for?
Variance controls how generic types relate when their type arguments are related. out T marks a producer position — the type only outputs T, so a Producer<Cat> can be used where a Producer<Animal> is expected (covariance). in T marks a consumer position — the type only accepts T, allowing the reverse relationship (contravariance). Kotlin's List<out T> is covariant, which is why a List<String> works where a List<Any> is wanted. For most everyday generic code you won't need to write variance modifiers, but they make library types like List flexible and safe.