kotlinadvanced

Kotlin Generics Variance In Out Star

Understand Kotlin generics: declaration-site variance with in/out, type projections, and star projection.

kotlin
// out = covariant (producer) — can only return T
interface Source<out T> {
    fun next(): T
}

// in = contravariant (consumer) — can only accept T
interface Consumer<in T> {
    fun consume(item: T)
}

// Invariant (default) — can both accept and return T
interface MutableBox<T> {
    fun get(): T
    fun set(value: T)
}

// Real-world example: Result container
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>() // Nothing is subtype of everything
}

// Comparator is contravariant
class AgeComparator : Comparator<Person> {
    override fun compare(a: Person, b: Person) = a.age.compareTo(b.age)
}

// Generic functions with upper bounds
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a >= b) a else b

// Multiple upper bounds
fun <T> ensureSerializable(value: T): T where T : Comparable<T>, T : java.io.Serializable {
    return value
}

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

// Reified to avoid type erasure
inline fun <reified T> List<*>.filterByType(): List<T> {
    return filterIsInstance<T>()
}

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int, val grade: String) : Person(name, age)

fun main() {
    // Covariance: Source<Student> is subtype of Source<Person>
    val studentSource: Source<Student> = object : Source<Student> {
        override fun next() = Student("Alice", 20, "A")
    }
    val personSource: Source<Person> = studentSource // OK! out variance
    println("Person: ${personSource.next().name}")

    // Contravariance: Consumer<Person> is subtype of Consumer<Student>
    val personConsumer: Consumer<Person> = object : Consumer<Person> {
        override fun consume(item: Person) = println("Consuming: ${item.name}")
    }
    val studentConsumer: Consumer<Student> = personConsumer // OK! in variance
    studentConsumer.consume(Student("Bob", 21, "B"))

    // Result with Nothing
    val success: Result<String> = Result.Success("Hello")
    val error: Result<String> = Result.Error("Oops") // Nothing fits any T

    when (success) {
        is Result.Success -> println("Got: ${success.value}")
        is Result.Error -> println("Error: ${success.message}")
    }

    // Star projection
    val mixed: List<*> = listOf(1, "hello", 3.14)
    printAll(mixed)

    // Filter by type
    val strings = mixed.filterByType<String>()
    println("Strings: $strings")

    // Upper bounds
    println("Max: ${maxOf(10, 20)}")
    println("Max: ${maxOf("apple", "banana")}")
}

Use Cases

  • Type-safe container hierarchies
  • Producer/consumer API design
  • Generic utility functions with constraints

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.