0
0
Kotlinprogramming~15 mins

In variance (contravariance) in Kotlin - Deep Dive

Choose your learning style9 modes available
Overview - In variance (contravariance)
What is it?
In Kotlin, contravariance is a way to make a generic type accept more general types than originally specified. It allows a type to be substituted with its supertypes, making the code more flexible when consuming data. This is done using the 'in' keyword before a generic type parameter. Contravariance is mainly used when a generic type only consumes values of a certain type but does not produce them.
Why it matters
Without contravariance, you would have to write many similar classes or functions for each specific type, making your code less reusable and harder to maintain. Contravariance solves this by allowing you to write more general and flexible code that can work with a range of types safely. This reduces bugs and improves code clarity, especially when dealing with collections or functions that only take inputs.
Where it fits
Before learning contravariance, you should understand Kotlin generics and basic variance concepts like covariance. After mastering contravariance, you can explore advanced topics like declaration-site variance, use-site variance, and how variance affects function types and collections in Kotlin.
Mental Model
Core Idea
Contravariance lets you use a more general type where a more specific type is expected, but only when you are consuming values, not producing them.
Think of it like...
Imagine a mail sorter who only receives letters to put into boxes. If the sorter can handle letters addressed to any family member (general), they can also handle letters addressed to a specific person (specific). Contravariance is like allowing the sorter to accept letters for any family member, not just one person.
Generic Type<T> Usage
┌─────────────────────────────┐
│       Contravariant 'in'    │
│                             │
│  Accepts supertypes of T     │
│  (More general types)        │
│                             │
│  Usage: consumes T values    │
└─────────────┬───────────────┘
              │
              ▼
  Example: Consumer<in T>
  Can accept Consumer<Any> where Consumer<String> expected
Build-Up - 7 Steps
1
FoundationUnderstanding Kotlin Generics Basics
🤔
Concept: Learn what generics are and why they help write reusable code.
Generics let you write classes and functions that work with any type. For example, List can hold any type T, like List or List. This avoids repeating code for each type.
Result
You can create flexible containers and functions that work with many types without rewriting code.
Understanding generics is essential because variance concepts like contravariance only make sense when you know how types can be generic placeholders.
2
FoundationIntroduction to Variance in Kotlin
🤔
Concept: Learn what variance means: covariance and contravariance control how generic types relate to each other.
Variance defines if a generic type can be substituted by its subtypes or supertypes. Covariance (using 'out') allows a generic type to produce values and accept subtypes. Contravariance (using 'in') allows a generic type to consume values and accept supertypes.
Result
You understand that variance controls type substitution rules for generics, making your code safer and more flexible.
Knowing variance prevents type errors and helps you design APIs that are both flexible and safe.
3
IntermediateContravariance with 'in' Keyword
🤔Before reading on: do you think 'in' allows passing subtypes or supertypes? Commit to your answer.
Concept: The 'in' keyword marks a generic type parameter as contravariant, meaning it can accept supertypes of the specified type.
In Kotlin, you declare contravariance by writing 'in' before a generic type parameter, like interface Consumer. This means Consumer can be replaced by Consumer because Any is a supertype of String. This works because the consumer only takes T as input and never produces it.
Result
You can safely substitute a consumer of a specific type with a consumer of a more general type.
Understanding that 'in' means accepting supertypes helps you design APIs that consume data flexibly without risking type errors.
4
IntermediatePractical Example: Consumer Interface
🤔Before reading on: can a Consumer be used where Consumer is expected? Commit to your answer.
Concept: Apply contravariance to a consumer interface that only takes input values.
Consider interface Consumer { fun consume(item: T) }. Since it only consumes T, you can assign Consumer to Consumer because Consumer can consume any String (since String is a subtype of Any). This is safe and flexible.
Result
You can reuse consumer implementations for broader types without rewriting code.
Knowing how contravariance works in practice helps you avoid unnecessary duplication and write more general code.
5
IntermediateContravariance Restrictions in Kotlin
🤔
Concept: Learn what operations are allowed or forbidden on contravariant types.
When a generic type parameter is marked 'in', you can only use it as input (function parameters). You cannot return it or use it as a property type for output. For example, val item: T is not allowed because it produces T. This restriction ensures type safety.
Result
You understand why Kotlin restricts how contravariant types are used to prevent runtime errors.
Knowing these restrictions helps you design your classes and interfaces correctly and avoid compiler errors.
6
AdvancedContravariance in Function Types
🤔Before reading on: do you think function parameter types are covariant or contravariant? Commit to your answer.
Concept: Function parameter types are contravariant, meaning you can use a function that accepts a supertype where a function accepting a subtype is expected.
In Kotlin, a function type like (String) -> Unit can be replaced by (Any) -> Unit because the function consuming Any can handle any String input. This is contravariance in action for function parameters. Return types are covariant.
Result
You can safely assign functions with broader parameter types to variables expecting narrower ones.
Understanding function type variance is key to using higher-order functions and callbacks safely and flexibly.
7
ExpertContravariance and Star Projections
🤔Before reading on: do star projections allow safe use of contravariant types? Commit to your answer.
Concept: Star projections provide a way to use generic types with unknown parameters safely, interacting with variance rules including contravariance.
When you use a star projection like Consumer<*>, Kotlin treats it as Consumer for contravariant parameters, meaning you can only safely consume nothing. This prevents unsafe operations when the exact type is unknown. Understanding this helps avoid subtle bugs in generic APIs.
Result
You can use star projections safely with contravariant types, knowing the compiler enforces restrictions.
Knowing how star projections interact with contravariance prevents confusing compiler errors and runtime issues in complex generic code.
Under the Hood
Kotlin's compiler enforces variance by restricting how generic type parameters are used in code. For contravariant parameters marked with 'in', the compiler ensures they are only used as input types (function parameters) and never as output types (return values or properties). This prevents type safety violations at runtime by disallowing operations that could produce a type mismatch. At runtime, Kotlin uses type erasure, so variance is a compile-time safety feature.
Why designed this way?
Contravariance was designed to allow safe substitution of generic types with their supertypes when only consuming values. This design balances flexibility and safety, avoiding the need for unsafe casts or duplicating code for each type. Alternatives like invariant generics would force rigid type matching, reducing code reuse and increasing errors.
Usage of Generic Type Parameter T
┌─────────────────────────────┐
│       Contravariant 'in'    │
│                             │
│  Allowed: function parameters│
│  Not allowed: return types   │
│                             │
│  Compiler enforces usage     │
└─────────────┬───────────────┘
              │
              ▼
  Safe substitution with supertypes
  (e.g., Consumer<Any> for Consumer<String>)
Myth Busters - 4 Common Misconceptions
Quick: Does contravariance allow passing subtypes instead of supertypes? Commit yes or no.
Common Belief:Contravariance means you can pass subtypes where supertypes are expected.
Tap to reveal reality
Reality:Contravariance allows passing supertypes where subtypes are expected, not the other way around.
Why it matters:Confusing this leads to type errors and unsafe code that can crash at runtime.
Quick: Can you use a contravariant type parameter as a return type? Commit yes or no.
Common Belief:You can use contravariant type parameters anywhere, including return types.
Tap to reveal reality
Reality:Contravariant type parameters can only be used as input (parameters), not as output (return types).
Why it matters:Ignoring this causes compiler errors and breaks type safety guarantees.
Quick: Does Kotlin's 'in' keyword mean the same as 'in' in other languages always? Commit yes or no.
Common Belief:The 'in' keyword in Kotlin always means the same as in other languages like Java.
Tap to reveal reality
Reality:Kotlin's 'in' keyword specifically marks contravariance at declaration-site variance, which differs from some other languages' usage.
Why it matters:Assuming identical behavior causes confusion and misuse of variance features.
Quick: Can star projections safely replace any generic type with contravariance? Commit yes or no.
Common Belief:Star projections allow safe use of any generic type regardless of variance.
Tap to reveal reality
Reality:Star projections impose restrictions and treat contravariant parameters as 'in Nothing', limiting usage.
Why it matters:Misunderstanding this leads to unexpected compiler errors and unsafe code assumptions.
Expert Zone
1
Contravariance only applies safely when the generic type parameter is used exclusively as input; mixing input and output breaks safety.
2
Function types in Kotlin are contravariant in their parameter types but covariant in their return types, a subtlety that affects higher-order function design.
3
Star projections interact with variance in complex ways, often defaulting contravariant parameters to 'Nothing', which can surprise even experienced developers.
When NOT to use
Avoid contravariance when your generic type needs to produce values of the generic type parameter or when you need invariant behavior. Instead, use covariance ('out') or invariant generics. For mutable collections that both consume and produce, invariance is safer.
Production Patterns
In production Kotlin code, contravariance is commonly used in consumer interfaces like event handlers, comparators, or processors that only accept input. It enables writing flexible APIs that can accept broader types without duplication. Also, function types leverage contravariance for parameter types to allow flexible callback assignments.
Connections
Covariance
Opposite variance concept; covariance allows substituting subtypes, contravariance allows supertypes.
Understanding contravariance alongside covariance completes the picture of how Kotlin manages type safety and flexibility in generics.
Function Types Variance
Contravariance applies to function parameter types, covariance to return types.
Knowing contravariance helps understand why Kotlin function types behave the way they do, enabling safe higher-order functions.
Type Theory (Computer Science)
Contravariance is a concept from type theory describing subtype relationships in function types.
Recognizing contravariance as a fundamental type theory concept connects programming practice to mathematical foundations, deepening understanding.
Common Pitfalls
#1Using contravariant type parameter as a return type causes compiler errors.
Wrong approach:interface Consumer { fun produce(): T }
Correct approach:interface Consumer { fun consume(item: T) }
Root cause:Contravariant parameters can only be used as input, not output, to maintain type safety.
#2Assigning Consumer to Consumer expecting contravariance.
Wrong approach:val consumerAny: Consumer = Consumer()
Correct approach:val consumerString: Consumer = Consumer()
Root cause:Contravariance allows substituting supertypes for subtypes, not the reverse.
#3Misusing star projections with contravariant types assuming full flexibility.
Wrong approach:val consumer: Consumer<*> = Consumer() consumer.consume("text") // compile error
Correct approach:val consumer: Consumer = Consumer() consumer.consume("text") // works
Root cause:Star projections limit contravariant parameters to 'Nothing', restricting usage.
Key Takeaways
Contravariance in Kotlin allows generic types to accept supertypes safely when only consuming values.
The 'in' keyword marks contravariant type parameters, restricting their usage to input positions only.
Function parameter types are contravariant, enabling flexible assignment of functions with broader parameter types.
Misusing contravariant types as outputs or confusing subtypes and supertypes leads to compiler errors and unsafe code.
Understanding contravariance alongside covariance and function type variance is essential for writing safe, reusable Kotlin code.