skip to content

Kotlin emphasizes declaration-site variance with out and in. At the 'how Kotlin models types' level, what problem does this solve and how does it differ from Java's approach?

level: seniorimportance: should knowfreq 55%

answer

  1. Invariant by default; variance relaxes it safely
  2. out = covariant = producer = return position
  3. in = contravariant = consumer = parameter position
  4. Declaration-site (once) vs use-site projections (per call)
  5. <*> = star projection, unknown-but-fixed type

basics

~20 s

Variance decides when a List of a subtype counts as a List of a supertype. Kotlin lets the library author declare this once on the type parameter (out for producers, in for consumers), so every user gets it automatically instead of repeating wildcards at each use.

solid answer

~50 s

Generics are invariant by default: `Box<Dog>` is not a `Box<Animal>`. Variance relaxes that safely. Kotlin's headline feature is **declaration-site variance**: the library author writes the modifier once on the type parameter. `out T` makes the parameter *covariant* — `T` may only appear in **out** positions (return types), so `List<out T>` (and the standard `List<T>`, declared `interface List<out E>`) makes `List<Dog>` a subtype of `List<Animal>`. `in T` makes it *contravariant* — `T` only in **in** positions (parameters), so `Comparable<in T>` lets a `Comparable<Number>` serve as `Comparable<Int>`. Java instead uses **use-site variance** (wildcards `? extends`/`? super`) repeated at every usage, which Kotlin still supports as **type projections** (`Array<out Any>`) for cases the declaration cannot fix. The star projection `<*>` means 'some unknown type, safely'. Declaration-site variance pushes the safety reasoning to one place and removes wildcard noise from callers.

code

kotlin · 5 lines
kotlin
interface Producer<out T> { fun get(): T }
interface Consumer<in T> { fun accept(t: T) }

val animals: Producer<Animal> = object : Producer<Dog> { override fun get() = Dog() }
val intC: Consumer<Int> = object : Consumer<Number> { override fun accept(t: Number) {} }

go deeper

for a junior

Recognizes that List<Dog> can be used as List<Animal> but cannot explain why.

for a middle

Correctly maps out to covariance/producers and in to contravariance/consumers with examples.

for a senior

Contrasts declaration-site vs use-site/projections, explains why mutable containers stay invariant, and uses star projection appropriately.

for a principal

Reasons about API design tradeoffs: where to push variance to the declaration, soundness of covariant reads, and migration of Java wildcard APIs into Kotlin.

## The variance problem Generics are **invariant** by default: even though `Dog` is a subtype of `Animal`, `MutableList<Dog>` is **not** a subtype of `MutableList<Animal>`. This must be so for mutable containers: if it were allowed you could add a `Cat` through the `Animal` view and corrupt the `Dog` list. Variance is the set of rules for *when* it is safe to relax invariance. ## Covariance — `out` A type is **covariant** in `T` if `Producer<Sub>` is a subtype of `Producer<Super>`. This is safe only when the type *produces* `T` (returns it) and never *consumes* it (takes it as a parameter). Kotlin marks this on the declaration: ```kotlin interface Source<out T> { // T only appears in 'out' positions fun next(): T } val anySource: Source<Any> = object : Source<String> { override fun next() = "x" } ``` The standard library declares `interface List<out E>`, so `List<Dog>` *is* a `List<Animal>`. The compiler enforces that a covariant `T` never appears as a function parameter. ## Contravariance — `in` A type is **contravariant** in `T` if `Consumer<Super>` is a subtype of `Consumer<Sub>`. Safe when the type only *consumes* `T`: ```kotlin interface Sink<in T> { fun put(value: T) } val intSink: Sink<Int> = object : Sink<Number> { override fun put(v: Number) {} } ``` `Comparable<in T>` works this way: a `Comparable<Number>` can be used where a `Comparable<Int>` is needed. ## Declaration-site vs use-site - **Declaration-site** (Kotlin's emphasis): the author writes `out`/`in` once on the parameter; *every* caller benefits with no extra syntax. The mnemonic is **PECS** baked into the declaration — *Producer-`out`, Consumer-`in`*. - **Use-site** (Java's wildcards): the caller repeats `? extends T` / `? super T` at each usage. Kotlin supports the same idea as **type projections**: `Array<out Any>` (read-only view) or `Array<in String>` (write-only view), used when a class is genuinely invariant (like `Array`) but a particular call site can be relaxed. ```kotlin fun copy(from: Array<out Any>, to: Array<Any>) { /* from is a projected, read-only source */ } ``` ## Star projection `<*>` `Box<*>` means 'a `Box` of some unknown but fixed type'. You may read its `out` values as the upper bound type and may not pass anything but `null` into `in` positions. It is the safe way to talk about a generic when the argument is irrelevant. ## Why Kotlin emphasizes declaration-site Most generic types are naturally producers or consumers, so declaring variance once removes the wildcard clutter Java callers carry, centralizes the safety proof, and makes subtyping between generic instantiations 'just work' in everyday code.

  • Why can't you mark MutableList's element type as out?
    MutableList both produces (get) and consumes (add) its element, so making it covariant would let you add the wrong subtype through a widened view; it must stay invariant.
  • When do you still need use-site projections in Kotlin?
    When the class is inherently invariant (e.g. Array, or a type that both produces and consumes T) but a specific call site only reads or only writes, so you project that single usage with out/in.

Declaration-site variance is labeling a pipe 'one-way out' or 'one-way in' at the factory, so plumbers never re-mark it at each junction.

saying these in an interview costs you the question

  • Swapping out and in (out is for producers/return, in for consumers/parameters)
  • Claiming all generics are covariant by default
  • Saying Kotlin has no use-site variance (it does, via projections)
  • Thinking out/in change runtime behavior rather than compile-time subtyping
  • Confusing star projection <*> with Any

context