Polyglot
Languages Kotlin generics
Kotlin § generics

Generics

Kotlin’s generics admit substantial type parameterisation: generic functions, generic classes, generic constraints (via where and :), declaration-site variance (in for contravariance, out for covariance), use-site variance (* for star projection), reified type parameters (in inline functions — admit runtime access to T), and type erasure (the JVM constraint). The combination — substantial type expressiveness, declaration-site variance for clean library design, reified parameters for runtime type access, the substantial standard-library generic surface — is the substance of Kotlin’s generic mechanism. The conventional Kotlin discipline favours covariant read-only collection types (List<out T>) and uses generics with bounds for substantial type-safe abstraction.

Generic functions

Type parameters in angle brackets after fun:

fun <T> identity(value: T): T = value

val s = identity("hello")                          // T inferred as String
val n = identity(42)                               // T inferred as Int
val list = identity(listOf(1, 2, 3))               // T inferred as List<Int>

For multiple type parameters:

fun <A, B> pair(a: A, b: B): Pair<A, B> = Pair(a, b)

val p = pair("hello", 42)                          // Pair<String, Int>

Generic constraints

The : admits upper bounds:

fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b

max(3, 5)                                          // 5
max("a", "b")                                      // "b"

fun <T : Number> sum(values: List<T>): Double {
    return values.sumOf { it.toDouble() }
}

sum(listOf(1, 2, 3))                               // 6.0
sum(listOf(1.5, 2.5))                              // 4.0

The constraint admits using the constrained operations on T.

For multiple bounds, the where clause:

fun <T> process(value: T) where T : Comparable<T>, T : Cloneable {
    // T must be Comparable<T> AND Cloneable
}

Generic classes

Type parameters in angle brackets after the class name:

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 size: Int get() = items.size
}

val s = Stack<Int>()
s.push(1)
s.push(2)
val top = s.pop()                                  // Int? (Optional)

The Stack<Int> is the instantiation; Stack<T> is the generic type.

Generic interfaces and inheritance

interface Container<T> {
    fun add(item: T)
    fun get(index: Int): T?
}

class ListContainer<T> : Container<T> {
    private val items = mutableListOf<T>()

    override fun add(item: T) { items.add(item) }
    override fun get(index: Int): T? = items.getOrNull(index)
}

For multiple parameters:

interface Map<K, V> {
    val size: Int
    operator fun get(key: K): V?
    fun containsKey(key: K): Boolean
}

Type inference

Kotlin admits substantial type inference:

fun <T> first(list: List<T>): T? = list.firstOrNull()

val n = first(listOf(1, 2, 3))                     // Int?
val s = first(listOf("a", "b"))                    // String?

// In contexts where inference fails, explicit args:
val empty = emptyList<String>()
val empty: List<String> = emptyList()              // alternative

Variance

Variance describes how generic types relate to each other when their type parameters are related.

Covariance with out

A covariant type parameter — List<Dog> is admitted where List<Animal> is expected (read-only):

class Animal
class Dog : Animal()

interface Producer<out T> {                        // T is covariant (out)
    fun produce(): T
}

val dogs: Producer<Dog> = ...
val animals: Producer<Animal> = dogs               // OK: Producer<Dog> is Producer<Animal>

The out admits the parameter only in return positions (not parameter positions). The mechanism admits substantial substitutability for read-only types.

Contravariance with in

A contravariant type parameter — Comparator<Animal> is admitted where Comparator<Dog> is expected (write/consume-only):

interface Consumer<in T> {                         // T is contravariant (in)
    fun consume(item: T)
}

val animalConsumer: Consumer<Animal> = ...
val dogConsumer: Consumer<Dog> = animalConsumer    // OK: Consumer<Animal> is Consumer<Dog>

The in admits the parameter only in parameter positions (not return).

Invariance (default)

Without in or out, the type parameter is invariantMutableList<Dog> is not admitted where MutableList<Animal> is expected:

interface MutableContainer<T> {                    // invariant
    fun add(item: T)
    fun get(): T
}

val dogs: MutableContainer<Dog> = ...
val animals: MutableContainer<Animal> = dogs       // ERROR: invariant

The conventional standard library uses:

  • List<out T> — covariant (read-only).
  • MutableList<T> — invariant (admits add/set).
  • Comparator<in T> — contravariant (consumes T).

Use-site variance with *

The * (star projection) admits “any subtype of the upper bound”:

fun process(list: List<*>) {
    // list is List<out Any?>
    println(list.size)
    val item = list[0]                             // Any?
}

process(listOf(1, 2, 3))
process(listOf("a", "b"))

The mechanism is conventional when the specific type doesn’t matter:

fun isEmpty(map: Map<*, *>): Boolean = map.isEmpty()

Reified type parameters

Inside inline functions, type parameters may be marked reified — admit runtime access to the type:

inline fun <reified T> isInstance(value: Any): Boolean {
    return value is T                              // T is admitted at runtime
}

isInstance<String>("hello")                        // true
isInstance<Int>("hello")                           // false

inline fun <reified T> Any.cast(): T? = this as? T

val n = "hello".cast<Int>()                        // null
val s = "hello".cast<String>()                     // "hello"

inline fun <reified T> List<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

val mixed: List<Any> = listOf(1, "two", 3, "four")
val ints = mixed.filterIsInstance<Int>()           // [1, 3]
val strings = mixed.filterIsInstance<String>()     // ["two", "four"]

The inline reified admits substantial runtime type access — the type parameter is substituted at the call site, admitting the is and as operations.

The principal restrictions:

  • Function must be inline.
  • No direct equivalent in regular generic functions — type erasure removes the type parameter.

Generic constraints with multiple types

fun <T> process(items: List<T>)
where T : Comparable<T>, T : Cloneable {
    // ...
}

The where admits multiple constraints; particularly useful when type parameters interact:

fun <K, V> putIfMissing(map: MutableMap<K, V>, key: K, value: V): V
where K : Comparable<K> {
    if (!map.containsKey(key)) {
        map[key] = value
    }
    return map[key]!!
}

Generic with Nothing

The Nothing type admits substantial type-system tricks:

sealed class Result<out T, out E>
class Success<T>(val value: T) : Result<T, Nothing>()
class Failure<E>(val error: E) : Result<Nothing, E>()

// Both Result<T, Nothing> and Result<Nothing, E> are admitted
// where Result<T, E> is expected (covariance + Nothing as universal subtype):
val ok: Result<Int, String> = Success(42)
val err: Result<Int, String> = Failure("oops")

Common patterns

Generic identity

fun <T> identity(x: T): T = x

fun <T> tap(value: T, action: (T) -> Unit): T {
    action(value)
    return value
}

val result = tap(getValue()) { v -> println("got: $v") }

Generic factory

inline fun <reified T> create(): T {
    return T::class.java.getDeclaredConstructor().newInstance()
}

class MyClass

val instance: MyClass = create<MyClass>()
val instance: MyClass = create()                   // type inferred from variable

Generic container

data class Box<T>(val value: T)

fun <T> wrap(value: T): Box<T> = Box(value)

val b = wrap(42)                                   // Box<Int>
val s = wrap("hello")                              // Box<String>

Generic transformation

inline fun <T, R> Iterable<T>.mapToList(transform: (T) -> R): List<R> {
    val result = mutableListOf<R>()
    for (item in this) {
        result.add(transform(item))
    }
    return result
}

val lengths = listOf("a", "bb", "ccc").mapToList { it.length }

Generic filter with type predicate

inline fun <reified T> List<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

val mixed: List<Any> = listOf(1, "a", 2, "b")
val ints: List<Int> = mixed.filterIsInstance()

Result type

sealed class Result<out T, out E> {
    data class Success<T>(val value: T) : Result<T, Nothing>()
    data class Failure<E>(val error: E) : Result<Nothing, E>()

    inline fun <R> map(transform: (T) -> R): Result<R, E> = when (this) {
        is Success -> Success(transform(value))
        is Failure -> this
    }

    inline fun <F> mapError(transform: (E) -> F): Result<T, F> = when (this) {
        is Success -> this
        is Failure -> Failure(transform(error))
    }
}

fun divide(a: Int, b: Int): Result<Int, String> {
    return if (b == 0) Result.Failure("division by zero")
           else Result.Success(a / b)
}

Repository pattern

interface Repository<T : Any, ID : Any> {
    suspend fun findById(id: ID): T?
    suspend fun save(entity: T): T
    suspend fun delete(id: ID)
}

class UserRepository : Repository<User, UUID> {
    override suspend fun findById(id: UUID): User? = /* ... */
    override suspend fun save(entity: User): User = /* ... */
    override suspend fun delete(id: UUID) = /* ... */
}

Generic with constraint chaining

inline fun <reified T> Json.parse(text: String): T
where T : Any {
    return decodeFromString<T>(text)
}

val user: User = Json.parse(jsonString)
val users: List<User> = Json.parse(jsonString)

KClass for generic factories

import kotlin.reflect.KClass

fun <T : Any> create(klass: KClass<T>): T {
    return klass.java.getDeclaredConstructor().newInstance()
}

// With reified, simpler:
inline fun <reified T : Any> create(): T = T::class.java.getDeclaredConstructor().newInstance()

Type-safe builders

class TableBuilder<T> {
    private val rows = mutableListOf<T>()

    fun row(item: T) { rows.add(item) }

    fun build(): List<T> = rows.toList()
}

inline fun <T> table(builder: TableBuilder<T>.() -> Unit): List<T> {
    return TableBuilder<T>().apply(builder).build()
}

val users = table<User> {
    row(User("Alice", 30))
    row(User("Bob", 25))
}

Type-aware extension function

inline fun <reified T : Any> List<Any>.firstInstanceOf(): T? {
    return firstOrNull { it is T } as? T
}

val mixed: List<Any> = listOf(1, "a", 2.0, true)
val firstString = mixed.firstInstanceOf<String>()  // "a"

Conditional type safety

inline fun <reified T> Any.castOrThrow(): T {
    return this as? T ?: throw ClassCastException("Cannot cast to ${T::class}")
}

Generic where with multiple bounds

fun <T> save(item: T) where T : Identifiable, T : Serializable {
    val data = item.serialize()
    storage[item.id] = data
}

A note on type erasure

The JVM erases generic type information at runtime — List<Int> and List<String> have the same runtime type:

val list: List<Int> = listOf(1, 2, 3)
list is List<Int>                                  // WARNING: cannot check; cast does nothing
list is List<*>                                    // OK: List<*> is the runtime type

val any: Any = list
when (any) {
    is List<*> -> println("a list")
    else -> println("other")
}

The reified admits substantial workaround for inline functions; outside inline functions, type information is erased.

A note on the conventional discipline

The contemporary Kotlin generics advice:

  • Use generics for genuinely generic code.
  • Use T : Bound constraints where applicable.
  • Use where clauses for substantial constraints.
  • Use declaration-site variance (in, out) for clean library design.
  • Use * for star projection when the specific type doesn’t matter.
  • Use reified in inline functions for runtime type access.
  • Trust type inference — explicit type arguments are rarely needed.
  • Use Nothing for type-system tricks (e.g., universal subtype in sum types).
  • Use KClass<T> for class references.

The combination — generic functions and classes, declaration-site variance, use-site star projection, reified type parameters in inline functions, the substantial standard-library generic surface — is the substance of Kotlin’s generics. The discipline produces substantial type-safe abstraction with substantial flexibility for library design.