Generic Variance (in / out)
Variance controls whether a generic type like Box<Dog> can stand in for Box<Animal> , using out for producers and in for consumers.
Learn Generic Variance (in / out) in our free Kotlin course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with examples you run in your browser, 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
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> .
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> .
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.
Your turn. Fill in the ___ blank, then run and compare.
Mini-Challenge: A Read-Only Source
Use out so a Source<Int> works where a Source<Number> is required.
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<*>
Frequently Asked Questions
🎉 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
- ✅ Next lesson: Building DSLs
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