Why is List<out E> declared covariant but MutableList<E> invariant in Kotlin?
answer
- out E = producer = covariant (List)
- MutableList consumes E via add → invariant
- Same hole as Java array covariance / ArrayStoreException
- Map covariant in V, invariant in K
- PECS: producer-out, consumer-in
basics
~10 sA read-only List<Cat> can safely be treated as a List<Animal> because you only read from it. A MutableList<Cat> cannot, because you could try to add a Dog, so it must stay exactly its type.
solid answer
~40 s`List<out E>` uses declaration-site covariance: because the read-only interface only *produces* `E` (returns it from `get`, `iterator`), it's safe to treat `List<Cat>` as `List<Animal>` — every cat you read is also an animal. `MutableList<E>` is **invariant**: it both produces and *consumes* `E` (e.g. `add(e: E)`). If `MutableList<Cat>` were a subtype of `MutableList<Animal>`, you could `add(aDog)` through the wider reference and corrupt the cats list — exactly the array-covariance hole Java has. So the mutable interface drops the `out` and is invariant in `E`. `Map<K, out V>` is covariant only in `V`, invariant in `K`, because keys are also used in lookups (consumed). This is the Producer-Extends / Consumer-Super (PECS) idea encoded at the declaration site via `out`.
code
kotlin · 10 linesopen class Animal
class Cat : Animal()
class Dog : Animal()
fun countAll(items: List<Animal>) = items.size
val cats: List<Cat> = listOf(Cat())
countAll(cats) // OK: List<Cat> is a List<Animal> (covariant)
// val m: MutableList<Animal> = mutableListOf(Cat()) // a MutableList<Cat> is NOT a MutableList<Animal>go deeper
May simply recall that List is more flexible to assign than MutableList without the variance terms.
Explains out E on List and invariance on MutableList using the add(Dog) example.
Ties it to PECS, use-site projections (MutableList<out E>), and Java array covariance unsafety.
Discusses declaration-site vs use-site variance trade-offs and API design implications for library authors.
## Variance refresher **Variance** describes how subtyping of a generic type relates to subtyping of its type argument. With `out` (covariance), `Box<Cat>` is a subtype of `Box<Animal>`. With `in` (contravariance) the relationship flips. **Invariant** means no relationship: `Box<Cat>` and `Box<Animal>` are unrelated. Kotlin supports **declaration-site variance**: you write `out`/`in` once on the interface, not at every use. ## Read-only List is covariant ```kotlin public interface List<out E> : Collection<E> { operator fun get(index: Int): E // produces E override fun iterator(): Iterator<E> } ``` `E` appears only in **out-positions** (return types). A `List` only *produces* elements, never accepts one to store. So `List<Cat>` can stand in for `List<Animal>` safely — anything you read is a `Cat`, which is an `Animal`. Hence `out E`. ## Mutable collections are invariant ```kotlin public interface MutableList<E> : List<E>, MutableCollection<E> { fun add(element: E): Boolean // consumes E operator fun set(index: Int, element: E): E } ``` Here `E` is in an **in-position** (`add` parameter). If `MutableList<Cat>` were also a `MutableList<Animal>`, this would compile and break at runtime: ```kotlin val cats: MutableList<Cat> = mutableListOf(Cat()) val animals: MutableList<Animal> = cats // HYPOTHETICAL - rejected by compiler animals.add(Dog()) // would put a Dog among Cats! val c: Cat = cats[1] // ClassCastException at runtime ``` Kotlin forbids the second line at **compile time** by making `MutableList` invariant. This is precisely the unsafe **array covariance** that Java allows (`Object[] a = new String[1]; a[0] = 1;` → `ArrayStoreException`). Kotlin's `Array<T>` is also invariant for the same reason. ## Map variance `Map<K, out V>`: covariant in the value (only produced), **invariant in `K`** because keys are used in `get`/`containsKey` (consumed). `MutableMap<K, V>` is invariant in both. ## Use-site variance escape hatch When you do need flexibility on a mutable type, use the type projection `MutableList<out E>` (use-site `out`) at a call boundary; the compiler then forbids the unsafe `add` there. This mirrors Java's `? extends`. The mnemonic is **PECS**: Producers use `out` (extends), Consumers use `in` (super).
- What is the use-site equivalent of List's covariance for a mutable type?A type projection: MutableList<out E>. It produces E safely but forbids calling add/set at that use site.
- Why is Map covariant only in V and not K?Values are only produced (read out), but keys are consumed by get/containsKey, so K must stay invariant.
A read-only list is a vending machine that only dispenses; a mutable list is one you can also load — and you must not load a Dog into the Cat machine.
saying these in an interview costs you the question
- Saying MutableList<Cat> is a subtype of MutableList<Animal>
- Not connecting invariance to the unsafe add(Dog) scenario
- Confusing in and out, or claiming Kotlin arrays are covariant like Java's
- Thinking covariance is a runtime feature rather than a compile-time subtyping rule