Generic Variance (in / out)
Reviewed & published by Brayan K
Variance controls whether a generic type like Box<Dog> can stand in for Box<Animal>, using out for producers and in for consumers.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll learn covariance with out, contravariance with in, why MutableList is invariant, and the star projection *.
What You'll Learn in This Lesson
- What variance means and why invariance is the default
- out (covariant) producers, like List
- in (contravariant) consumers, like Comparator
- Why MutableList must stay invariant
- The star projection * for unknown types
- Declaration-site vs use-site variance
1️⃣ out: Covariant Producers
Marking a type parameter out T promises the class only ever produces T (returns it), never consumes it. That makes it safe for a Box<Dog> to be used as a Box<Animal> — every dog you read out is also an animal. This is why List is declared List<out E>.
// 'out' = covariant: the type only ever comes OUT (a producer).
// A Box<Dog> can be used where a Box<Animal> is expected.
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
class Box<out T>(private val item: T) {
fun get(): T = item // T only flows OUT
}
fun printName(box: Box<Animal>) {
println(box.get().name)
}
fun main() {
val dogBox: Box<Dog> = Box(Dog("Rex"))
// Because Box is 'out', a Box<Dog> IS a Box<Animal>:
printName(dogBox) // -> Rex
}2️⃣ in: Contravariant Consumers
Marking in T promises the class only ever consumes T (takes it as input), never returns it. This reverses the direction: a Sink<Animal> can be used where a Sink<Dog> is expected, because anything that handles any animal can handle a dog. This is why Comparator is Comparator<in T>.
// 'in' = contravariant: the type only ever goes IN (a consumer).
// A Sink<Animal> can be used where a Sink<Dog> is expected.
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
class Sink<in T> {
fun accept(item: T) { // T only flows IN
println("Accepted")
}
}
fun feedDog(sink: Sink<Dog>) {
sink.accept(Dog("Rex"))
}
fun main() {
// A Sink that accepts ANY Animal can certainly accept a Dog:
val animalSink: Sink<Animal> = Sink()
// Because Sink is 'in', a Sink<Animal> IS a Sink<Dog>:
feedDog(animalSink) // -> Accepted
}3️⃣ Invariance and the Star Projection
A type that both produces and consumes — like MutableList<T> — can't be made in or out safely, so it stays invariant and its arguments must match exactly. When you don't care about the element type at all, use the star projection List<*> for a safe, read-only view.
// MutableList is INVARIANT (it both produces AND consumes T),
// so it has no in/out and types must match exactly.
// The star projection * means "some unknown type" — read only.
fun countItems(list: List<*>): Int = list.size
fun main() {
val ints = listOf(1, 2, 3)
val words = listOf("a", "b")
// List<*> accepts any List because we only READ its size:
println(countItems(ints)) // -> 3
println(countItems(words)) // -> 2
}Your turn. Fill in the ___ blank, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
open class Animal(val name: String)
class Cat(name: String) : Animal(name)
// 1) Make Container a producer so Container<Cat> works as Container<Animal>
class Container<___ T>(private val value: T) { // 👉 put: out
fun read(): T = value
}
fun describe(c: Container<Animal>) = println(c.read().name)
fun main() {
val catBox: Container<Cat> = Container(Cat("Mimi"))
describe(catBox)
// ✅ Expected output:
// Mimi
}Mini-Challenge: A Read-Only Source
Use out so a Source<Int> works where a Source<Number> is required.
// 🎯 MINI-CHALLENGE: a read-only Source.
// Save as Source.kt and run main().
//
// 1. Make Source<out T> with a fun next(): T.
// 2. A Source<Int> should be usable where a Source<Number> is needed.
// 3. In main(), pass a Source<Int> to a function taking Source<Number>
// and print the value.
//
// ✅ Expected output:
// 42
class Source<out T>(private val value: T) {
fun next(): T = value
}
fun firstNumber(s: Source<Number>): Number = s.next()
fun main() {
val ints: Source<Int> = Source(42)
// print firstNumber(ints)
}Common Errors (and the fix)
- ❌ "Type parameter T is declared as 'out' but occurs in 'in' position" — you took T as a parameter. ✅ A producer can't consume; remove the input or drop out.
- ❌ "... occurs in 'out' position" — an in T returned T. ✅ A consumer can't produce; don't return T.
- ❌ "Type mismatch: required MutableList<Animal>" — you passed a MutableList<Dog>. ✅ It's invariant; the type arguments must match exactly.
- ❌ Trying to add to a List<*> — the element type is unknown. ✅ Star projections are read-mostly; you can't safely add.
Pro Tips
- 💡 PECS: "Producer Extends, Consumer Super" from Java maps to Kotlin's out (producer) and in (consumer).
- 💡 Prefer declaration-site: annotate the class with out/in when you own it; use projections only for invariant classes you don't.
- 💡 Read-only collections are covariant: that's why List<Dog> is a List<Animal> but MutableList is not.
📋 Quick Reference — Variance
| Modifier | Role | Example |
|---|---|---|
| out T | producer (covariant) | List<out E> |
| in T | consumer (contravariant) | Comparator<in T> |
| T | both (invariant) | MutableList<T> |
| * | unknown (read-only) | List<*> |
🎉 Lesson Complete!
- ✅ Generics are invariant by default
- ✅ out T = covariant producer (like List)
- ✅ in T = contravariant consumer (like Comparator)
- ✅ MutableList both reads and writes, so it stays invariant
- ✅ List<*> is a safe read-only view of an unknown type
Practice quiz
What does variance control in generics?
- How fast a generic compiles
- Whether Box<Dog> can stand in for Box<Animal>
- How many type parameters are allowed
- Whether a type can be null
Answer: Whether Box<Dog> can stand in for Box<Animal>. Variance governs the subtype relationship between Box<Dog> and Box<Animal>.
By default, Kotlin generics are…
- Covariant
- Contravariant
- Invariant
- Nullable
Answer: Invariant. Without an annotation, generics are invariant — no subtype relationship.
What does the 'out' modifier make a type parameter?
- A covariant producer
- A contravariant consumer
- Invariant
- Reified
Answer: A covariant producer. out marks a covariant producer: T only flows out (is returned).
Which standard type is declared Comparator<in T>?
- List
- Box
- Comparator
- Map
Answer: Comparator. Comparator is contravariant: Comparator<in T> consumes values of T.
With 'in T', which substitution is allowed?
- Sink<Dog> where Sink<Animal> is expected
- Nothing can substitute
- Sink<Animal> where Sink<Dog> is expected
- Only exact matches
Answer: Sink<Animal> where Sink<Dog> is expected. in reverses the relationship: a Sink<Animal> works where Sink<Dog> is needed.
Why must MutableList stay invariant?
- It is immutable
- It both produces and consumes T
- It has no type parameter
- It only consumes T
Answer: It both produces and consumes T. MutableList reads (produces) and adds (consumes) T, so neither relaxation is safe.
What does the star projection List<*> give you?
- A writable list of Any
- A safe read-only view of an unknown element type
- A null list
- A covariant MutableList
Answer: A safe read-only view of an unknown element type. List<*> is a list of some unknown type, usable as a read-only view.
If 'out T' occurs as a function parameter (in position), what happens?
- It works fine
- It is silently ignored
- It auto-converts to in
- It is a compile error
Answer: It is a compile error. A producer can't consume T, so taking out T as a parameter is a compile error.
How does Java's PECS map to Kotlin?
- Producer -> out, Consumer -> in
- Producer -> in, Consumer -> out
- Both map to invariant
- Both map to *
Answer: Producer -> out, Consumer -> in. Producer Extends -> out; Consumer Super -> in.
Where does declaration-site variance put the annotation?
- At each usage site
- On the class itself, like class Box<out T>
- On the main function
- Only in Java code
Answer: On the class itself, like class Box<out T>. Declaration-site variance annotates the class once, e.g. class Box<out T>.
Continue this course
- Previous: Type Aliases & Value Classes
- Next: Building DSLs (Lambdas with Receiver) — Create expressive builder DSLs with function types that have a receiver
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What does variance mean in generics?
Variance answers the question: if Dog is a subtype of Animal, what is the relationship between Box<Dog> and Box<Animal>? By default Kotlin generics are invariant, meaning Box<Dog> and Box<Animal> have no subtype relationship at all — neither can substitute for the other. Variance annotations (out and in) relax this in a type-safe way: 'out' makes the relationship follow the type argument (covariant), and 'in' reverses it (contravariant). The rule the compiler enforces keeps you from ever putting the wrong type in or getting the wrong type out.
When do I use 'out' (covariance)?
Use 'out T' when your class only ever produces or returns values of T and never consumes them as parameters — it is a producer. The canonical example is List<out E>: you can read elements from it but not add to it, so a List<Dog> can safely be treated as a List<Animal> (every Dog you read out is also an Animal). Mark the type parameter 'out' and the compiler will reject any method that takes T as an input, guaranteeing the substitution is sound.
When do I use 'in' (contravariance)?
Use 'in T' when your class only ever consumes values of T as inputs and never produces them — it is a consumer. The classic example is Comparator<in T>: a Comparator<Animal> that can compare any two animals can certainly compare two dogs, so a Comparator<Animal> can be used where a Comparator<Dog> is expected. This reverses the usual direction: with 'in', the more general type (Animal) substitutes for the more specific one (Dog). The compiler forbids returning T from such a class.
Why is MutableList invariant?
MutableList<T> both produces T (when you read with get) and consumes T (when you add). If it were covariant you could assign a MutableList<Dog> to a MutableList<Animal> and then add a Cat to it — corrupting the original list of dogs. If it were contravariant you could read an element typed as Dog that was actually some other Animal. Because it does both, neither relaxation is safe, so MutableList must be invariant and its type arguments must match exactly. List (read-only) avoids this and is covariant.
What is the star projection (List<*>)?
The star projection, written List<*> (or Box<*>), means 'a List of some specific but unknown element type'. It is useful when you do not care about the element type — for example counting elements with list.size or printing them as strings. Because the exact type is unknown, you can read elements only as the upper bound (often Any?) and you cannot safely add anything except null. Think of it as a safe, read-mostly view when the concrete type argument genuinely does not matter to your code.
What is the difference between declaration-site and use-site variance?
Declaration-site variance puts the annotation on the class itself, like class Box<out T>, so every use of Box is automatically covariant — this is the cleaner, more common style in Kotlin. Use-site variance puts the annotation at a specific usage instead, like a parameter of type Box<out Animal> (called a type projection), when the class itself is invariant but a particular function only reads from it. Java's wildcards (? extends, ? super) are purely use-site; Kotlin supports both but prefers declaration-site when you own the class.