In variance (contravariance) in Kotlin - Time & Space Complexity
Let's explore how the time cost changes when using in variance (contravariance) in Kotlin generics.
We want to see how the program's work grows as input size changes when using contravariant types.
Analyze the time complexity of the following Kotlin code using in variance.
interface Consumer {
fun consume(item: T)
}
fun feedAll(consumer: Consumer, items: List) {
for (item in items) {
consumer.consume(item)
}
}
This code feeds a list of integers to a consumer that accepts numbers using contravariance.
Look for loops or repeated actions that affect time.
- Primary operation: The
forloop callingconsumefor each item. - How many times: Once for every item in the
itemslist.
As the number of items grows, the loop runs more times, so work grows directly with input size.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 10 consume calls |
| 100 | 100 consume calls |
| 1000 | 1000 consume calls |
Pattern observation: The work increases in a straight line as input size grows.
Time Complexity: O(n)
This means the time to run grows directly with the number of items to consume.
[X] Wrong: "Using in variance changes how many times the loop runs or makes it slower."
[OK] Correct: The variance only affects type safety and compatibility, not how many times the loop executes.
Understanding how variance affects code helps you reason about type safety and performance separately, a useful skill in real projects.
What if we changed Consumer<Number> to Consumer<Any>? How would the time complexity change?
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
