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

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)

Pro Tips

📋 Quick Reference — Generics

ConceptKotlin Syntax
Generic classclass Box<T>(val item: T)
Generic functionfun <T> firstOf(l: List<T>): T
Two paramsfun <K, V> pairUp(k: K, v: V)
Upper bound<T : Comparable<T>>
Explicit typeBox<String>("hi")

🎉 Lesson Complete!

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

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.