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 invariant — MutableList<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 : Boundconstraints where applicable. - Use
whereclauses 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
reifiedininlinefunctions for runtime type access. - Trust type inference — explicit type arguments are rarely needed.
- Use
Nothingfor 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.