Kotlin Academy · Lesson

Declaration-Site Variance

in and out.

Lesson 2 of 413 steps

Declaration-Site Variance is a free Kotlin Academy lesson on CoddyKit. This is lesson 2 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the Kotlin Academy learning path, and your progress syncs across the web and the CoddyKit app. The Kotlin Academy course includes 4 lessons in total.

The Variance Problem

Is a List<String> a kind of List<Any>? Sometimes yes, sometimes no. Variance describes how generic types relate when their type parameters relate. Kotlin uses out and in to express this.

Invariance by Default

By default generics are invariant: Box<String> is NOT a Box<Any>, even though String is a subtype of Any. This prevents unsafe operations.

class Box<T>(val value: T)

fun main() {
    val strBox = Box("hi")
    // val anyBox: Box<Any> = strBox // would not compile
    println(strBox.value)
}

Covariance with out

Marking a type parameter out makes it covariant: Producer<String> becomes a subtype of Producer<Any>. The type can only appear in output positions (return types).

class Producer<out T>(private val item: T) {
    fun produce(): T = item
}

fun main() {
    val strProducer: Producer<String> = Producer("hello")
    val anyProducer: Producer<Any> = strProducer
    println(anyProducer.produce())
}

Why out Is Safe

If a class only produces (returns) T and never consumes it, treating a Producer<String> as a Producer<Any> is safe, because every String it returns is also an Any.

out in the Standard Library

Kotlin's read-only List<out E> is covariant, which is why a List<String> can be used where a List<Any> is expected.

fun printAll(items: List<Any>) {
    items.forEach { println(it) }
}

fun main() {
    val names: List<String> = listOf("Ann", "Bob")
    printAll(names)
}

Contravariance with in

Marking a type parameter in makes it contravariant: Consumer<Any> becomes a subtype of Consumer<String>. The type can only appear in input positions (parameters).

class Consumer<in T> {
    fun consume(item: T) {
        println("Consumed $item")
    }
}

fun main() {
    val anyConsumer: Consumer<Any> = Consumer()
    val strConsumer: Consumer<String> = anyConsumer
    strConsumer.consume("hi")
}

Why in Is Safe

A consumer that accepts Any can certainly accept a String. So a Consumer<Any> can stand in wherever a Consumer<String> is needed.

The PECS Mnemonic

Remember Producer Extends, Consumer Super, or in Kotlin terms: out for producers (output), in for consumers (input). A type that only outputs T uses out; one that only inputs T uses in.

Combining in and out

A type parameter used for both input and output (like in a mutable list) must stay invariant. Function types show both: parameters are in, the return is out.

fun main() {
    val f: (Number) -> Int = { it.toInt() }
    val g: (Int) -> Number = f
    println(g(3.0.let { Number::class; 5 }))
}

Declaration-Site Means One Place

Declaration-site variance is declared once, on the class itself with in/out. Every use of the class then gets the variance automatically, unlike Java where you repeat wildcards at each use.

Choosing in or out

Ask: does this class only produce T (use out), only consume T (use in), or both (leave invariant)? Matching the modifier to the role makes your generic types flexible and safe.

Quick Check

What does the out modifier indicate?

Recap

Generics are invariant by default. Use out for covariance when a type only produces values, and in for contravariance when it only consumes values. Remember producers use out, consumers use in. Next you will apply variance at the point of use with type projections.

Free to start

Learn Kotlin with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
51
Lessons
203

Frequently Asked Questions

Is the “Declaration-Site Variance” lesson free?

Yes — any 3 lessons of the Kotlin Academy learning path, including “Declaration-Site Variance”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The Kotlin Academy course includes 4 lessons in total.

What will I learn in “Declaration-Site Variance”?

in and out. You practise Kotlin Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Kotlin Academy?

No prior experience is required. Kotlin Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.

How long does the “Declaration-Site Variance” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Kotlin Academy lesson?

Yes. Every Kotlin Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Generic Functions and Classes
  2. Declaration-Site Variance
  3. Use-Site Variance
  4. Generic Constraints
← Back to Kotlin Academy