0Pricing
Kotlin Academy · Lesson

Declaration-Site Variance: in and out

Use covariant (out) and contravariant (in) type parameters correctly.

Declaration-Site Variance: in and out is a free Kotlin Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Variance Problem

In Kotlin, List<String> is a subtype of List<Any> because List is declared with out T. Without variance annotations, this would not hold.

val strings: List<String> = listOf("a", "b")
val anys: List<Any> = strings  // OK because List<out T>
// MutableList<String> is NOT a subtype of MutableList<Any>:
// val m: MutableList<Any> = mutableListOf("x") // compile error
fun main() { println(anys) }

Covariance with out

out T means the class can only produce T values (return them), never consume them. This makes Producer<Dog> a subtype of Producer<Animal>.

interface Producer<out T> {
    fun produce(): T
}
class DogProducer : Producer<String> {
    override fun produce() = "Woof!"
}
fun sound(p: Producer<Any>) = println(p.produce())
fun main() {
    val dog = DogProducer()
    sound(dog)  // OK: Producer<String> is a subtype of Producer<Any>
}

Contravariance with in

in T means the class can only consume T values (accept them as parameters), never produce them. This makes Consumer<Animal> a subtype of Consumer<Dog>.

interface Consumer<in T> {
    fun consume(item: T)
}
class Printer : Consumer<Any> {
    override fun consume(item: Any) = println(item)
}
fun feedDog(consumer: Consumer<String>) = consumer.consume("Dog")
fun main() {
    val printer = Printer()
    feedDog(printer)  // OK: Consumer<Any> is a subtype of Consumer<String>
}

out Restriction: No in-position

With out T, T cannot appear in parameter (in) position. The compiler enforces this.

interface ReadOnly<out T> {
    fun get(): T          // OK: out position
    // fun set(t: T) {}  // Error: T in in-position violates out variance
}
class Box<out T>(private val value: T) : ReadOnly<T> {
    override fun get() = value
}
fun main() {
    val box: ReadOnly<Any> = Box<String>("hello")
    println(box.get())
}

in Restriction: No out-position

With in T, T cannot appear in return (out) position.

interface WriteOnly<in T> {
    fun set(t: T)        // OK: in position
    // fun get(): T {}  // Error: T in out-position violates in variance
}
class Sink<in T> : WriteOnly<T> {
    override fun set(t: T) = println("Received: $t")
}
fun main() {
    val sink: WriteOnly<String> = Sink<Any>()
    sink.set("hello")
}

Kotlin Standard Library Examples

Comparable<in T> is contravariant: a Comparable<Number> can compare any Number subtype. List<out T> is covariant: a list of cats is a list of animals.

fun sortNumbers(list: List<Number>) = list.sortedWith(compareBy { it.toDouble() })
fun main() {
    val ints: List<Int> = listOf(3, 1, 2)
    val sorted = sortNumbers(ints)  // OK: List<Int> is List<Number>
    println(sorted)
}

Invariance: MutableList

MutableList<T> is invariant: it can both produce and consume T, so no subtype relationship holds between different T types.

fun addNumber(list: MutableList<Number>) { list.add(1.5) }
fun main() {
    val ints = mutableListOf<Int>(1, 2)
    // addNumber(ints) // compile error: MutableList<Int> is not MutableList<Number>
    val nums = mutableListOf<Number>(1, 2)
    addNumber(nums)   // OK
    println(nums)
}

Use-Site Variance as Alternative

When you can't change the class, use use-site variance: out at the call site to project a type to covariant.

fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
    for (item in from) to.add(item)
}
fun main() {
    val src = mutableListOf("a", "b", "c")
    val dest = mutableListOf<Any>()
    copy(src, dest)
    println(dest)
}

Practical: Repository Pattern

Use covariance for read-only repositories and contravariance for write-only sinks to model clean data flow.

interface Repository<out T> {
    fun findAll(): List<T>
    fun findById(id: Int): T?
}
interface Writer<in T> {
    fun save(entity: T)
}
interface ReadWriteRepo<T> : Repository<T>, Writer<T>
// ReadWriteRepo<User> is neither sub nor super of ReadWriteRepo<Admin>

Variance Decision Guide

Choose variance based on how T is used: out = producer (only returns T), in = consumer (only accepts T), invariant = both (MutableList, Channel).

// Quick mental model:
// out T: source/producer  — List, Flow, Sequence
// in  T: sink/consumer    — Comparable, Continuation
// invariant: read+write   — MutableList, Channel, MutableStateFlow
fun main() {
    val nums: List<Number> = listOf(1, 2, 3)  // List is out
    println(nums)
}

Invariant Class with out Function

Even in an invariant class you can use out at use-site for specific function parameters.

class Stack<T>(private val items: MutableList<T> = mutableListOf()) {
    fun push(item: T) = items.add(item)
    fun pop(): T? = items.removeLastOrNull()
}
fun printAll(stack: Stack<out Any>) {
    // can only read, not write
    println(stack.pop())
}
fun main() {
    val s = Stack<String>()
    s.push("hello")
    printAll(s)
}

Quick Check

What does out T on a type parameter mean?

Recap

out T (covariance) allows subtyping when producing values. in T (contravariance) allows subtyping when consuming values. Invariant types (both read and write) have no subtype relationship across different T.

Frequently asked questions

Is the “Declaration-Site Variance: in and out” lesson free?

Yes — the full text of “Declaration-Site Variance: in and out” is free to read here on the web, and the Kotlin Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Kotlin Academy course, upgrade to CoddyKit PRO.

What will I learn in “Declaration-Site Variance: in and out”?

Use covariant (out) and contravariant (in) type parameters correctly. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Kotlin Academy?

No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Declaration-Site Variance: in and out” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Kotlin Academy lesson?

Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Generic Functions and Type Constraints with where
  2. Declaration-Site Variance: in and out
  3. Star Projection and When to Use *
  4. Type Erasure and reified Type Parameters
← Back to Kotlin Academy