Bird
Raised Fist0
Kotlinprogramming~15 mins

Out variance (covariance) in Kotlin - Deep Dive

Choose your learning style10 modes available
Overview - Out variance (covariance)
What is it?
Out variance, also known as covariance, is a way to make generic types flexible when they produce values. It means you can use a more specific type where a more general type is expected, but only when the generic type is used for output. This helps Kotlin keep your code safe while allowing useful reuse of types.
Why it matters
Without out variance, you would have to write many similar classes or functions for each specific type, making your code repetitive and harder to maintain. It also prevents bugs by ensuring you only use types in safe ways, avoiding mistakes like putting the wrong type into a container. This makes your programs more reliable and easier to understand.
Where it fits
Before learning out variance, you should understand basic generics and type parameters in Kotlin. After mastering out variance, you can learn about in variance (contravariance) and how variance affects function parameters and return types in more complex scenarios.
Mental Model
Core Idea
Out variance means a generic type can safely produce (output) values of a subtype, allowing flexible and safe reuse of types.
Think of it like...
Imagine a vending machine that only gives out snacks. If the machine is designed to give out any kind of snack, you can safely replace it with a machine that gives out only chocolate bars, because chocolate bars are a kind of snack. But you can’t put snacks back into the machine, only take them out.
Generic Type<T> (Producer)
  β”‚
  β–Ό
Can be replaced by Generic Type<Subtype of T>

Example:
List<Number> can be replaced by List<Int> if List is out-variant

Kotlin syntax:
interface Producer<out T> {
  fun produce(): T
}
Build-Up - 7 Steps
1
FoundationUnderstanding Generics Basics
πŸ€”
Concept: Learn what generics are and how they let you write flexible code that works with different types.
Generics let you create classes or functions that can work with any type. For example, a Box can hold any type T. This avoids writing many similar classes for each type.
Result
You can create a Box or Box from the same Box class.
Understanding generics is essential because variance only applies to generic types, so you need this foundation first.
2
FoundationWhy Type Safety Matters
πŸ€”
Concept: Learn why Kotlin prevents mixing incompatible types to avoid bugs.
If you put a String into a Box, your program might crash or behave unexpectedly. Kotlin uses type checks to stop this. Variance helps keep these checks safe while allowing flexibility.
Result
Kotlin will not let you assign Box to Box without variance rules.
Knowing why Kotlin enforces type safety helps you appreciate why variance rules exist.
3
IntermediateIntroducing Out Variance
πŸ€”Before reading on: do you think a List can be used where a List is expected? Commit to your answer.
Concept: Out variance allows a generic type to produce values of a subtype safely, enabling substitution of more specific types where general types are expected.
In Kotlin, you mark a generic type with 'out' to say it only produces values. For example, List means you can use List where List is expected because String is a subtype of Any.
Result
You can assign List to a variable of type List if List is declared with out variance.
Understanding out variance unlocks safe flexibility in your code, letting you reuse generic types with subtypes without risking type errors.
4
IntermediateHow Out Variance Restricts Usage
πŸ€”Before reading on: do you think you can add elements to a List? Commit to your answer.
Concept: Out variance restricts the generic type to only produce values, preventing unsafe operations like adding elements.
When a generic type is marked 'out', you cannot use it as a consumer (input). For example, you cannot add elements to a List because Kotlin cannot guarantee the exact type.
Result
Trying to add elements to a List causes a compile error.
Knowing these restrictions prevents common mistakes and helps you understand why variance exists to keep code safe.
5
IntermediateUsing Out Variance in Interfaces
πŸ€”
Concept: Learn how to declare interfaces with out variance to make them flexible producers.
You can declare an interface like interface Producer { fun produce(): T } which means Producer can produce values of type T or its subtypes. This allows assigning Producer to Producer safely.
Result
You can assign Producer to Producer because Int is a subtype of Number.
Using out variance in interfaces is a common pattern that enables flexible and safe API design.
6
AdvancedOut Variance and Function Types
πŸ€”Before reading on: do you think function return types can be out-variant? Commit to your answer.
Concept: Function return types are naturally covariant (out variant), meaning functions returning subtypes can replace functions returning supertypes.
In Kotlin, a function type like () -> T is covariant in T. This means a function returning Int can be used where a function returning Number is expected. This matches the out variance concept.
Result
You can assign a function returning Int to a variable expecting a function returning Number.
Recognizing function return types as covariant helps you understand variance in real Kotlin code.
7
ExpertVariance and Type Projections Internals
πŸ€”Before reading on: do you think Kotlin creates new types at runtime for variance? Commit to your answer.
Concept: Kotlin uses type projections and compiler checks to enforce variance at compile time without creating new runtime types.
Variance in Kotlin is a compile-time feature. The compiler checks that you only use generic types in allowed ways (produce-only for out). At runtime, the JVM sees the erased types without variance info. This means variance does not affect performance but ensures safety.
Result
Your program runs normally with no runtime overhead from variance.
Understanding that variance is a compile-time safety feature explains why it doesn't slow down your program and how Kotlin balances flexibility and safety.
Under the Hood
Out variance works by restricting how generic types are used in code. When a generic type parameter is marked 'out', Kotlin's compiler enforces that this type is only used in output positions (like return types) and never as input (like function parameters). This restriction allows the compiler to safely treat a generic type with a subtype as a subtype of the generic type with a supertype. At runtime, Kotlin uses type erasure, so variance does not exist as a separate entity; it is purely a compile-time check.
Why designed this way?
Variance was designed to solve the problem of safely reusing generic types with different type arguments without losing type safety. Early generic systems were either too strict or unsafe. Kotlin's approach balances flexibility and safety by enforcing variance rules at compile time, avoiding runtime overhead and preventing common bugs caused by incorrect type substitutions.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Generic Type<T>             β”‚
β”‚                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚  β”‚ Out Variance  β”‚          β”‚
β”‚  β”‚ (Producer)    β”‚          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚         β”‚                   β”‚
β”‚         β–Ό                   β”‚
β”‚  Use T only as output       β”‚
β”‚  (e.g., return values)      β”‚
β”‚                             β”‚
β”‚  Compiler enforces rules    β”‚
β”‚  at compile time            β”‚
β”‚                             β”‚
β”‚  Runtime: Type erasure      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚
          β–Ό
  Safe substitution:
  Generic<Subtype> <: Generic<Supertype>
Myth Busters - 3 Common Misconceptions
Quick: Can you add elements to a List? Commit to yes or no.
Common Belief:Because List is a list, you can add elements to it like a normal list.
Tap to reveal reality
Reality:You cannot add elements to a List because the exact type T is unknown, and adding could break type safety.
Why it matters:Trying to add elements causes compile errors and confusion, leading to frustration and misuse of variance.
Quick: Does out variance mean you can use any subtype anywhere? Commit to yes or no.
Common Belief:Out variance means you can freely substitute any subtype for any generic type without restrictions.
Tap to reveal reality
Reality:Out variance only applies when the generic type is used for output (producing values). It does not allow unsafe substitutions when the type is used for input.
Why it matters:
Quick: Does Kotlin create new runtime types for each variance? Commit to yes or no.
Common Belief:Kotlin creates different runtime types for each variance annotation to enforce safety.
Tap to reveal reality
Reality:Variance is a compile-time feature only; at runtime, Kotlin uses type erasure and does not create new types.
Why it matters:Thinking variance affects runtime can cause confusion about performance and debugging.
Expert Zone
1
Out variance only applies to generic type parameters used in output positions; mixing input and output usage requires careful design or separate interfaces.
2
Kotlin's declaration-site variance (using 'out' in the generic declaration) differs from use-site variance (type projections), and understanding both is key for advanced API design.
3
Variance annotations do not affect JVM bytecode but influence Kotlin's type checker, which means interoperability with Java requires attention to Java's wildcards.
When NOT to use
Do not use out variance when your generic type needs to consume values of the generic type (e.g., adding elements). Instead, use in variance (contravariance) or invariant types. For mutable collections, use invariant types or separate producer and consumer interfaces.
Production Patterns
In production, out variance is commonly used in read-only collections like Kotlin's List and in producer interfaces that only return values. It helps create flexible APIs that accept subtypes safely without exposing mutation methods.
Connections
In variance (contravariance)
Opposite variance concept where generic types consume values instead of producing them.
Understanding out variance helps grasp in variance because they are two sides of the same coin controlling safe type substitution.
Java Generics Wildcards
Kotlin's out variance corresponds to Java's '? extends T' wildcard usage.
Knowing Kotlin's out variance clarifies how Java's wildcards work, aiding interoperability and cross-language understanding.
Type Systems in Natural Languages
Variance resembles how words can have broader or narrower meanings depending on context (e.g., 'animal' vs 'dog').
Recognizing variance as a way to handle subtype relationships in types connects programming to how meaning shifts in language, deepening conceptual understanding.
Common Pitfalls
#1Trying to add elements to a covariant (out) generic type.
Wrong approach:val list: List = listOf(1, 2, 3) list.add(4)
Correct approach:val list: MutableList = mutableListOf(1, 2, 3) list.add(4)
Root cause:Misunderstanding that out variance restricts the generic type to output-only, so adding elements is unsafe and disallowed.
#2Using out variance on a generic type that both consumes and produces values.
Wrong approach:interface Box { fun put(item: T) // Error: T used in input position fun get(): T }
Correct approach:interface Producer { fun get(): T } interface Consumer { fun put(item: T) }
Root cause:Confusing variance rules by mixing input and output usage in the same generic parameter.
Key Takeaways
Out variance (covariance) allows generic types to safely produce values of subtypes, enabling flexible and reusable code.
Marking a generic type parameter with 'out' restricts it to output positions only, preventing unsafe operations like adding elements.
Variance is a compile-time feature in Kotlin that enforces type safety without affecting runtime performance.
Understanding out variance is essential for designing safe and flexible APIs, especially for read-only collections and producer interfaces.
Misusing variance leads to compile errors or unsafe code, so knowing when and how to apply it is crucial for robust Kotlin programming.

Practice

(1/5)
1. What does the out keyword mean in Kotlin generics?
easy
A. The generic type can be used for both input and output.
B. The generic type is used only for input (consuming values).
C. The generic type is used only for output (producing values).
D. The generic type is mutable and can be changed.

Solution

  1. Step 1: Understand the meaning of out in Kotlin generics

    The out keyword means the generic type is only used to produce values, not consume them.
  2. Step 2: Compare options with this meaning

    The generic type is used only for output (producing values). correctly states that the generic type is used only for output (producing values). Other options describe input or mutability, which are incorrect.
  3. Final Answer:

    The generic type is used only for output (producing values). -> Option C
  4. Quick Check:

    Out variance = output only [OK]
Hint: Remember: out means output only, no input allowed [OK]
Common Mistakes:
  • Confusing out with in variance (input)
  • Thinking out allows mutation
  • Assuming out means both input and output
2. Which of the following is the correct syntax to declare a covariant generic interface in Kotlin?
easy
A. interface Producer { fun produce(): T }
B. interface Producer { fun produce(): T }
C. interface Producer<T> { fun produce(input: T) }
D. interface Producer<T> { fun produce(): T }

Solution

  1. Step 1: Identify covariance syntax in Kotlin

    Covariance is declared using out before the generic type parameter, like out T.
  2. Step 2: Match syntax with options

    interface Producer { fun produce(): T } correctly uses out T and a function that returns T. Other options either use in or lack the out keyword.
  3. Final Answer:

    interface Producer<out T> { fun produce(): T } -> Option A
  4. Quick Check:

    Covariant syntax = out T [OK]
Hint: Covariant generics use 'out' before the type parameter [OK]
Common Mistakes:
  • Using 'in' instead of 'out' for covariance
  • Omitting 'out' keyword
  • Confusing input and output functions
3. What will be the output of the following Kotlin code?
interface Source {
    fun next(): T
}

class StringSource : Source<String> {
    override fun next() = "Hello"
}

fun printSource(source: Source<Any>) {
    println(source.next())
}

fun main() {
    val stringSource = StringSource()
    printSource(stringSource)
}
medium
A. Hello
B. Compilation error due to type mismatch
C. Runtime error
D. Prints null

Solution

  1. Step 1: Understand covariance in the interface

    The interface Source is declared with out T, so Source<String> can be used where Source<Any> is expected.
  2. Step 2: Trace the function call and output

    stringSource returns "Hello" from next(). Passing it to printSource works due to covariance, so it prints "Hello".
  3. Final Answer:

    Hello -> Option A
  4. Quick Check:

    Covariance allows subtype substitution = Hello [OK]
Hint: Covariance lets Source<String> be used as Source<Any> safely [OK]
Common Mistakes:
  • Expecting a compilation error due to type mismatch
  • Confusing covariance with invariance
  • Thinking runtime error occurs
4. Identify the error in this Kotlin code using out variance:
interface Producer {
    fun produce(input: T): T
}
medium
A. No error, code is correct
B. Missing return type in function produce
C. Interface must be declared with in instead of out
D. Cannot use T as a function parameter when declared with out

Solution

  1. Step 1: Check usage of generic type T with out variance

    When T is declared with out, it can only be used as a return type, not as a function parameter.
  2. Step 2: Analyze the function signature

    The function produce(input: T): T uses T as a parameter, which is not allowed with out T.
  3. Final Answer:

    Cannot use T as a function parameter when declared with out -> Option D
  4. Quick Check:

    Out variance forbids T as input parameter [OK]
Hint: Out means T only as output, not input parameters [OK]
Common Mistakes:
  • Ignoring variance rules for function parameters
  • Thinking out allows input parameters
  • Confusing in and out variance
5. Given the following Kotlin classes:
open class Animal
class Dog : Animal()

interface Producer {
    fun produce(): T
}

fun feedAnimals(producer: Producer<Animal>) {
    val animal = producer.produce()
    println("Feeding an animal")
}

fun main() {
    val dogProducer: Producer<Dog> = object : Producer<Dog> {
        override fun produce() = Dog()
    }
    feedAnimals(dogProducer)
}

Why does this code compile and run without errors?
hard
A. Because Kotlin ignores variance annotations at runtime
B. Because Producer is covariant with out, allowing Producer<Dog> where Producer<Animal> is expected
C. Because produce() returns a generic type without restrictions
D. Because Dog is a superclass of Animal, so substitution is allowed

Solution

  1. Step 1: Understand covariance with out keyword

    The interface Producer is declared with out T, making it covariant. This means Producer<Dog> can be used where Producer<Animal> is expected.
  2. Step 2: Analyze class hierarchy and function call

    Dog is a subtype of Animal. The function feedAnimals expects Producer<Animal>, but dogProducer is Producer<Dog>. Covariance allows this substitution safely.
  3. Final Answer:

    Because Producer is covariant with out, allowing Producer<Dog> where Producer<Animal> is expected -> Option B
  4. Quick Check:

    Covariance allows subtype substitution [OK]
Hint: Covariance lets Producer<Dog> be used as Producer<Animal> [OK]
Common Mistakes:
  • Thinking Dog is superclass of Animal
  • Ignoring variance and expecting error
  • Assuming produce() return type is unrestricted
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard