kotlinadvanced

Generics — Variance, Bounds, and Type Projections

Master Kotlin generics: in/out variance, upper bounds, type projections, and generic constraints.

kotlin
// Covariance (out) — producer, read-only
interface Source<out T> {
    fun next(): T
}

// Contravariance (in) — consumer, write-only
interface Sink<in T> {
    fun put(item: T)
}

// Invariant — both reading and writing
interface Container<T> {
    fun get(): T
    fun set(item: T)
}

// Covariant list — safe to use List<Dog> as List<Animal>
open class Animal(val name: String)
class Dog(name: String, val breed: String) : Animal(name)
class Cat(name: String) : Animal(name)

fun printAnimals(animals: List<Animal>) {
    animals.forEach { println(it.name) }
}

// Upper bound constraint
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a >= b) a else b

// Multiple bounds (where clause)
fun <T> copyIfValid(item: T): String where T : Comparable<T>, T : CharSequence {
    return if (item.length > 0) item.toString() else "empty"
}

// Generic class with bound
class SortedList<T : Comparable<T>> {
    private val items = mutableListOf<T>()

    fun add(item: T) {
        items.add(item)
        items.sort()
    }

    fun getAll(): List<T> = items.toList()
    override fun toString() = items.toString()
}

// Star projection
fun printAll(items: List<*>) {
    items.forEach { println(it) }
}

// Type-safe heterogeneous container
class TypeMap {
    private val map = mutableMapOf<Class<*>, Any>()

    fun <T : Any> put(type: Class<T>, value: T) {
        map[type] = value
    }

    @Suppress("UNCHECKED_CAST")
    fun <T : Any> get(type: Class<T>): T? = map[type] as? T

    inline fun <reified T : Any> put(value: T) = put(T::class.java, value)
    inline fun <reified T : Any> get(): T? = get(T::class.java)
}

// Generic extension
fun <T> T.toSingletonList(): List<T> = listOf(this)
fun <T : Comparable<T>> T.coerceIn(range: ClosedRange<T>): T = when {
    this < range.start -> range.start
    this > range.endInclusive -> range.endInclusive
    else -> this
}

fun main() {
    // Covariance — List is covariant (out)
    val dogs: List<Dog> = listOf(Dog("Rex", "Lab"), Dog("Spot", "Beagle"))
    printAnimals(dogs) // Works! List<Dog> is subtype of List<Animal>

    // Upper bound
    println(maxOf(3, 7))       // 7
    println(maxOf("abc", "xyz")) // xyz

    // SortedList
    val sorted = SortedList<Int>()
    sorted.add(5); sorted.add(1); sorted.add(3)
    println("Sorted: $sorted") // [1, 3, 5]

    // TypeMap
    val config = TypeMap()
    config.put("Hello")
    config.put(42)
    config.put(3.14)
    println("String: ${config.get<String>()}")
    println("Int: ${config.get<Int>()}")
    println("Double: ${config.get<Double>()}")

    // Extensions
    println(42.toSingletonList()) // [42]
    println(150.coerceIn(0..100)) // 100
}

Use Cases

  • Type-safe generic APIs and containers
  • Variance declarations for library design
  • Bounded type constraints for safety

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.