Skip to content
intermediate Phase 1 · Kotlin Foundations

Generics

Use generic classes, functions, and type constraints for reusable, type-safe code.

40m
2 problems
Topic Progress 0%

Generic Classes and Functions

Why Generics?

Without generics, you write separate implementations for every type or use Any (which loses type safety). Generics let you write a single class or function that works with any type while preserving compile-time type checking.

// Without generics: type-unsafe
class Box {
    var value: Any? = null
}
val box = Box()
box.value = "hello"
val text: String = box.value as String  // Unsafe cast

// With generics: type-safe
class Box<T>(var value: T)
val box = Box("hello")
val text: String = box.value  // No cast needed

Generic Classes

A generic class accepts a type parameter (conventionally T, E, K, V):

class Pair<A, B>(val first: A, val second: B)

val pair = Pair(1, "one")  // Pair<Int, String>
println(pair.first)         // 1 (Int)
println(pair.second)        // one (String)

Type parameters can be constrained. If a function needs to call methods on the type, add an upper bound:

class StringBox<T : String>(val value: T) {
    fun length() = value.length
}

val box = StringBox("Hello")
println(box.length())  // 5
// val intBox = StringBox(42)  // Compile error: Int is not a String

The : String constraint means T must be String or a subtype of String.

Generic Functions

Generic functions declare type parameters in angle brackets before the function name:

fun <T> singletonList(item: T): List<T> {
    return listOf(item)
}

val list = singletonList(42)  // List<Int>
val names = singletonList("Kotlin")  // List<String>

Type inference usually determines T automatically. When it cannot, specify explicitly:

fun <T> emptyList(): List<T> = emptyList()
val empty = emptyList<String>()  // Explicit type needed

Generic Constraints

Use where to require multiple bounds:

fun <T> process(item: T) where T : Comparable<T>, T : Serializable {
    // T must implement both Comparable and Serializable
}

// Or with a single bound:
fun <T : Comparable<T>> sort(list: List<T>): List<T> {
    return list.sorted()
}

Variance: In and Out

The Variance Problem

Consider a simple holder class:

class Box<T>(val value: T)

If Dog is a subtype of Animal, is Box a subtype of Box? In Kotlin, the answer depends on variance. By default, generics are invariant: Box is NOT a subtype of Box, even though Dog is a subtype of Animal. This prevents you from putting a Cat into a Box.

Out (Covariance)

Mark a type parameter out if the class only produces (reads) values of type T. This makes the generic covariant: Box becomes a subtype of Box:

interface Source<out T> {
    fun next(): T
}

fun demo(strs: Source<String>) {
    val objects: Source<Any> = strs  // Allowed because of out
}

With out, T can only appear in output positions (return types). You cannot pass T as a function parameter.

In (Contravariance)

Mark a type parameter in if the class only consumes (writes) values of type T. This makes the generic contravariant: Box becomes a subtype of Box:

interface Comparable<in T> {
    fun compareTo(other: T): Int
}

fun demo(x: Comparable<Animal>) {
    x.compareTo(Dog())  // Allowed because of in
}

With in, T can only appear in input positions (function parameters). You cannot use T as a return type.

Star Projection

Use * when you do not care about the type parameter, similar to ? in Java wildcards:

fun printAll(list: List<*>) {
    for (item in list) {
        println(item)
    }
}

printAll(listOf(1, "hello", 3.14))

Type Erasure

At runtime, generic type information is erased. You cannot check T is String at runtime. Use reified type parameters with inline functions to preserve type information:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

println(isType<String>("hello"))  // true
println(isType<Int>("hello"))     // false

The reified keyword keeps the type information available at runtime by inlining the function body.

Practice Problems

0 / 3 solved
Generic Stack

Implement a generic Stack<T> class with push(item: T), pop(): T?, and peek(): T? operations. Use a mutableList as the backing store.

Solution
class Stack<T> {
    private val elements = mutableListOf<T>()

    fun push(item: T) {
        elements.add(item)
    }

    fun pop(): T? {
        if (elements.isEmpty()) return null
        return elements.removeAt(elements.lastIndex)
    }

    fun peek(): T? {
        return elements.lastOrNull()
    }

    fun isEmpty(): Boolean = elements.isEmpty()

    fun size(): Int = elements.size
}

fun main() {
    val stack = Stack<Int>()
    stack.push(1)
    stack.push(2)
    stack.push(3)
    println(stack.pop())    // 3
    println(stack.peek())   // 2
    println(stack.size())   // 2
}
Generic Filter Function

Write a generic function filterByType<T> that takes a List<Any> and returns a List<T> containing only elements of type T.

Solution
inline fun <reified T> filterByType(list: List<Any>): List<T> {
    return list.filterIsInstance<T>()
}

fun main() {
    val mixed = listOf(1, "hello", 2, "world", 3.14)
    val strings = filterByType<String>(mixed)
    println(strings)  // [hello, world]

    val ints = filterByType<Int>(mixed)
    println(ints)  // [1, 2]
}
Contravariant Transformer

Create an interface Transformer<in T> with a transform(value: T): String method. Implement it for Transformer<Any> and pass it where Transformer<String> is expected.

Solution
interface Transformer<in T> {
    fun transform(value: T): String
}

val anyTransformer = object : Transformer<Any> {
    override fun transform(value: Any): String {
        return "Value: $value (${value::class.simpleName})"
    }
}

fun processStrings(transformer: Transformer<String>) {
    println(transformer.transform("hello"))
    println(transformer.transform("world"))
}

fun main() {
    // Transformer<Any> can be used where Transformer<String> is expected
    processStrings(anyTransformer)
    // Value: hello (String)
    // Value: world (String)
}

Quiz

1. What does the 'out' keyword mean in a generic type parameter?

Question 1 options

2. Why are generics erased at runtime in Kotlin?

Question 2 options

3. What is the upper bound of a generic type parameter by default?

Question 3 options

4. When should you use reified type parameters?

Question 4 options

Flashcards

Question

What is the difference between in and out on generic type parameters?

Answer

out (covariance) means the type only appears in output positions. The generic becomes a producer. in (contravariance) means the type only appears in input positions. The generic becomes a consumer.

Question

What is type erasure in Kotlin?

Answer

At runtime, generic type information is removed by the JVM. You cannot check T is String at runtime unless you use reified type parameters in inline functions.

Question

How do you constrain a generic type parameter?

Answer

Use an upper bound: class Box<T : String> means T must be String or subtype. Use where for multiple bounds: fun <T> f() where T : A, T : B.

Question

What does star projection (*) mean in Kotlin generics?

Answer

Star projection is similar to Java wildcards. Use it when you do not care about the specific type. Example: List<*> accepts any List regardless of its type parameter.

Revision Notes

Key Takeaways

  • 1. Generics provide type safety while allowing code reuse across types.
  • 2. out makes a type covariant (producer); in makes it contravariant (consumer).
  • 3. Type erasure removes generic type info at runtime; reified preserves it for inline functions.
  • 4. Upper bounds restrict which types can be used as type parameters.
  • 5. Star projection (*) is Kotlin's equivalent of Java wildcards.

Interview Tips

  • Explain variance with concrete examples: why Box<Dog> is not Box<Animal> by default.
  • Know the difference between in (contravariance) and out (covariance) with real-world analogies.
  • Understand type erasure and when reified types are needed.
  • Be ready to implement a generic data structure like Stack or Queue.

Cheat Sheet

Generics Cheat Sheet

Generic Class:

  • class Box<T>(val value: T)
  • Type parameter T can be any type

Generic Function:

  • fun <T> item(value: T): T = value
  • Declare before function name

Upper Bound:

  • class Box<T : Comparable<T>>
  • T must implement Comparable
  • Multiple bounds: where T : A, T : B

Variance:

  • out T — covariant (producer, output only)
  • in T — contravariant (consumer, input only)
  • Default — invariant (neither)

Star Projection:

  • List<*> — type-agnostic usage
  • Cannot add elements (type unknown)

Reified Types:

  • inline fun <reified T> check(v: Any) = v is T
  • Preserves type info at runtime