0
0
Kotlinprogramming~15 mins

Out variance (covariance) in Kotlin - Deep Dive

Choose your learning style9 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.