What if you could pass a list of cats to a function expecting animals without any hassle or errors?
Why In variance (contravariance) in Kotlin? - Purpose & Use Cases
Imagine you have a list of animals and you want to pass it to a function that only accepts lists of cats. You try to do this manually by checking and converting each item, but it quickly becomes confusing and error-prone.
Manually converting or checking types every time you want to pass data between functions is slow and leads to mistakes. You might accidentally allow wrong types or write lots of repetitive code that is hard to maintain.
In variance with contravariance, Kotlin lets you safely use a more general type where a more specific type is expected. This means you can pass a list of cats to a function expecting animals without extra checks, making your code cleaner and safer.
fun feedCats(cats: List<Cat>) { /* feed cats */ }
val animals: List<Animal> = listOf(Dog(), Cat())
// Can't pass animals to feedCats directlyfun feedCats(cats: List<out Cat>) { /* feed cats */ }
val cats: List<Cat> = listOf(Cat(), Cat())
feedCats(cats) // Allowed with covarianceYou can write flexible functions that accept a range of related types safely, reducing code duplication and errors.
Think of a pet store app where you want to feed all animals that are cats or their subtypes without rewriting feeding logic for each animal subtype.
Manual type checks slow down development and cause bugs.
Covariance allows safe use of more specific types where general ones are expected.
This leads to cleaner, safer, and more flexible Kotlin code.
Practice
in keyword mean when used in Kotlin generics?Solution
Step 1: Understand variance keywords in Kotlin generics
Kotlin usesinfor contravariance andoutfor covariance.Step 2: Identify the meaning of
ininmeans the generic type can accept supertypes safely as input but cannot produce output of that type.Final Answer:
It marks a generic type as contravariant, allowing it to accept supertypes safely. -> Option AQuick Check:
in= contravariant = accept supertypes [OK]
- Confusing 'in' with 'out' variance
- Thinking 'in' allows output of the generic type
- Assuming 'in' restricts to exact types only
Solution
Step 1: Recall syntax for contravariant generic declaration
Contravariance usesinbefore the generic type parameter in the interface declaration.Step 2: Check method parameter usage
Method parameter can use the generic type normally; the variance is declared at the interface level.Final Answer:
interface Consumer { fun consume(item: T) } -> Option BQuick Check:
Contravariant syntax =inbefore T [OK]
- Using 'out' instead of 'in' for contravariance
- Placing 'in' inside method parameters incorrectly
- Omitting variance keyword entirely
interface Consumer {
fun consume(item: T)
}
fun feed(consumer: Consumer<Number>) {
consumer.consume(42)
}
val intConsumer: Consumer<Int> = object : Consumer<Int> {
override fun consume(item: Int) {
println("Consumed: $item")
}
}
feed(intConsumer)What will happen when this code runs?
Solution
Step 1: Understand contravariance direction
Contravariance means Consumer<Supertype> can accept Consumer<Subtype>, not the other way around.Step 2: Analyze the call feed(intConsumer)
feed expects Consumer<Number>, but intConsumer is Consumer<Int>. Since Int is subtype of Number, Consumer<Int> is not subtype of Consumer<Number> when contravariant.Final Answer:
Compilation error because Consumer<Int> is not a subtype of Consumer<Number>. -> Option CQuick Check:
Contravariance reverses subtype relation [OK]
- Assuming Consumer<Int> can be passed where Consumer<Number> is expected
- Confusing covariance and contravariance
- Expecting runtime error instead of compile-time error
interface Consumer {
fun consume(item: T)
}
fun process(consumer: Consumer<Number>) {
consumer.consume(3.14)
}
val doubleConsumer: Consumer<Double> = object : Consumer<Double> {
override fun consume(item: Double) {
println("Consumed double: $item")
}
}
process(doubleConsumer)Why does this code cause a compilation error?
Solution
Step 1: Recall contravariance subtype rules
ForConsumer, Consumer<Supertype> can accept Consumer<Subtype>, not the reverse.Step 2: Check the types in the call
process expects Consumer<Number>, but doubleConsumer is Consumer<Double>. Double is subtype of Number, so Consumer<Double> is not subtype of Consumer<Number>.Final Answer:
Because Consumer<Double> cannot be passed where Consumer<Number> is expected due to contravariance. -> Option AQuick Check:
Contravariance reverses subtype relation [OK]
- Thinking Double is not subtype of Number
- Confusing method parameter types
- Assuming runtime error instead of compile-time
Number or any of its supertypes safely. Which declaration correctly uses contravariance to allow this flexibility?interface Consumer {
fun consume(item: T)
}
fun acceptConsumer(consumer: Consumer<___>) {
consumer.consume(100)
}Solution
Step 1: Understand contravariance usage
Contravariance allows passing a consumer of a supertype to a function expecting a consumer of a subtype.Step 2: Determine the correct generic type
To accept consumers that can consume Number or any supertype safely (i.e., Consumer<Number> and Consumer<Any>), useAnyas the generic parameter.Final Answer:
Any -> Option DQuick Check:
Contravariant consumer accepts supertypes [OK]
- Using supertype like Any, which only accepts Consumer<Any>
- Using subtype like Int
- Using Nothing which is subtype of all types
Ready to prove it?
Pass the test and this pattern moves unknown
