skip to content

How do Kotlin's `out` and `in` variance modifiers map onto Java's `? extends` and `? super` wildcards, and how does this relate to the PECS rule?

level: juniorimportance: must knowfreq 70%

answer

  1. out = producer = ? extends
  2. in = consumer = ? super
  3. PECS = Producer Extends, Consumer Super
  4. covariant reads, contravariant writes
  5. copy(from: out, to: in)

basics

~10 s

out means a type only produces values, like Java's ? extends (a producer). in means a type only consumes values, like Java's ? super (a consumer). PECS: Producer Extends, Consumer Super.

solid answer

~40 s

Kotlin uses declaration-site and use-site variance instead of Java's wildcard-only model. `out T` (covariant, producer) corresponds to Java's `? extends T`: you can read `T` out but not pass it in. `in T` (contravariant, consumer) corresponds to `? super T`: you can pass `T` in but only read `Any?` out. This is exactly Josh Bloch's PECS rule — Producer-Extends, Consumer-Super. Kotlin's `MutableList<out Number>` compiles to `List<? extends Number>`; `MutableList<in Number>` compiles to `List<? super Number>`. The key insight is that Kotlin lets you declare variance once on the class (`interface List<out E>`) so callers rarely need wildcards, whereas Java forces the wildcard at every use site.

code

kotlin · 7 lines
kotlin
fun <T> copy(from: Array<out T>, to: Array<in T>) {
    for (i in from.indices) to[i] = from[i]   // read producer, write consumer
}

val ints: Array<Int> = arrayOf(1, 2)
val anys: Array<Any> = arrayOfNulls<Any>(2)
copy(ints, anys)   // from: Array<out Int>, to: Array<in Int> — both OK

go deeper

for a junior

Recites the out=extends / in=super mapping and the PECS mnemonic with a simple example.

for a middle

Explains WHY producers are covariant and consumers contravariant, and writes a correct copy signature.

for a senior

Connects declaration-site vs use-site variance and predicts the generated Java wildcard for a given Kotlin signature.

for a principal

Discusses API design tradeoffs of where to place variance and how it affects cross-language consumers and source compatibility.

## The problem variance solves Generics are *invariant* by default: `List<String>` is NOT a subtype of `List<Any>`, even though `String` is a subtype of `Any`. Variance modifiers selectively relax this. ## Covariance — `out` / `? extends` A type is a **producer** if it only *returns* (produces) `T` and never *accepts* it as a parameter. - Kotlin: `out T` (declaration-site) or `Source<out T>` (use-site projection). - Java equivalent: `? extends T`. - Rule: if it only produces `T`, then `Producer<Cat>` can safely be used as `Producer<Animal>` (you'll only ever pull out things that are at least `Animal`). ```kotlin interface Source<out T> { fun next(): T } // producer val cats: Source<Cat> = ... val animals: Source<Animal> = cats // OK: covariant ``` In Java the same thing requires a wildcard at the use site: `Source<? extends Animal>`. ## Contravariance — `in` / `? super` A type is a **consumer** if it only *accepts* `T` as a parameter and never returns it. - Kotlin: `in T` or `Sink<in T>`. - Java equivalent: `? super T`. - Rule: `Sink<Animal>` can be used as `Sink<Cat>` — anything that consumes `Animal` can certainly consume `Cat`. ```kotlin interface Sink<in T> { fun put(item: T) } // consumer val animalSink: Sink<Animal> = ... val catSink: Sink<Cat> = animalSink // OK: contravariant ``` ## PECS — the mnemonic Josh Bloch (Effective Java) coined **PECS**: **P**roducer **E**xtends, **C**onsumer **S**uper. When you read FROM a structure, use `extends` (`out`); when you write TO it, use `super` (`in`). Kotlin's `copy` signature is the canonical example: ```kotlin fun <T> copy(from: Array<out T>, to: Array<in T>) { /* read from, write to */ } ``` `from` is a producer (`out`), `to` is a consumer (`in`). This maps directly to Java's `<T> void copy(T[] src /* ? extends T */, T[] dst /* ? super T */)`. ## Why the mapping matters Kotlin compiles `out`/`in` to real Java wildcards in bytecode, so the two languages interoperate seamlessly. Understanding the correspondence lets you predict the generated Java signature and write APIs that read naturally from both sides. | Kotlin | Java | Role | |--------|------|------| | `out T` | `? extends T` | producer (read) | | `in T` | `? super T` | consumer (write) | | `T` (invariant) | `T` | both |

  • Why can't you call `put(item: T)` on a `Source<out T>`?
    Because `out` forbids `T` in `in` (parameter) positions; allowing writes would break covariance safety, so the compiler rejects it.
  • What is `out`/`in` called formally?
    `out` is covariance, `in` is contravariance; declaring them on the class is declaration-site variance.

A vending machine is out (it only dispenses); a recycling bin is in (it only accepts).

saying these in an interview costs you the question

  • Saying `out` means 'output type' generally rather than a producer/covariant position
  • Mixing up PECS — claiming Producer Super
  • Thinking Kotlin has no equivalent to Java wildcards
  • Believing `in T` lets you read `T` back out as `T` (you only get `Any?`)

context