Out variance (covariance) in Kotlin - Time & Space Complexity
We want to understand how the time cost changes when using out variance (covariance) in Kotlin generics.
Specifically, how does the program's work grow as input size changes when we use out variance?
Analyze the time complexity of the following code snippet.
interface Producer {
fun produce(): T
}
class StringProducer : Producer {
override fun produce() = "Hello"
}
fun useProducer(producer: Producer) {
println(producer.produce())
}
fun main() {
val stringProducer = StringProducer()
useProducer(stringProducer)
}
This code shows how out variance allows a Producer of String to be used where Producer of Any is expected.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Calling produce() once to get a value.
- How many times: Exactly once per call; no loops or recursion involved.
Since produce() is called only once, the work does not grow with input size.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | 1 |
| 100 | 1 |
| 1000 | 1 |
Pattern observation: The number of operations stays the same no matter how big the input is.
Time Complexity: O(1)
This means the time to run this code stays constant regardless of input size.
[X] Wrong: "Using out variance makes the program slower because it adds extra work."
[OK] Correct: Out variance only affects type safety at compile time and does not add extra runtime work or loops.
Understanding how variance affects program behavior helps you explain type safety and performance clearly in interviews.
"What if produce() was called inside a loop of size n? How would the time complexity change?"
Practice
out keyword mean in Kotlin generics?Solution
Step 1: Understand the meaning of
Theoutin Kotlin genericsoutkeyword means the generic type is only used to produce values, not consume them.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.Final Answer:
The generic type is used only for output (producing values). -> Option CQuick Check:
Out variance = output only [OK]
- Confusing out with in variance (input)
- Thinking out allows mutation
- Assuming out means both input and output
Solution
Step 1: Identify covariance syntax in Kotlin
Covariance is declared usingoutbefore the generic type parameter, likeout T.Step 2: Match syntax with options
interface Producer { fun produce(): T } correctly usesout Tand a function that returnsT. Other options either useinor lack theoutkeyword.Final Answer:
interface Producer<out T> { fun produce(): T } -> Option AQuick Check:
Covariant syntax =out T[OK]
- Using 'in' instead of 'out' for covariance
- Omitting 'out' keyword
- Confusing input and output functions
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)
}Solution
Step 1: Understand covariance in the interface
The interfaceSourceis declared without T, soSource<String>can be used whereSource<Any>is expected.Step 2: Trace the function call and output
stringSourcereturns "Hello" fromnext(). Passing it toprintSourceworks due to covariance, so it prints "Hello".Final Answer:
Hello -> Option AQuick Check:
Covariance allows subtype substitution = Hello [OK]
- Expecting a compilation error due to type mismatch
- Confusing covariance with invariance
- Thinking runtime error occurs
interface Producer {
fun produce(input: T): T
}Solution
Step 1: Check usage of generic type T with out variance
WhenTis declared without, it can only be used as a return type, not as a function parameter.Step 2: Analyze the function signature
The functionproduce(input: T): TusesTas a parameter, which is not allowed without T.Final Answer:
Cannot use T as a function parameter when declared with out -> Option DQuick Check:
Out variance forbids T as input parameter [OK]
- Ignoring variance rules for function parameters
- Thinking out allows input parameters
- Confusing in and out variance
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?
Solution
Step 1: Understand covariance with out keyword
The interfaceProduceris declared without T, making it covariant. This meansProducer<Dog>can be used whereProducer<Animal>is expected.Step 2: Analyze class hierarchy and function call
Dogis a subtype ofAnimal. The functionfeedAnimalsexpectsProducer<Animal>, butdogProducerisProducer<Dog>. Covariance allows this substitution safely.Final Answer:
Because Producer is covariant with out, allowing Producer<Dog> where Producer<Animal> is expected -> Option BQuick Check:
Covariance allows subtype substitution [OK]
- Thinking Dog is superclass of Animal
- Ignoring variance and expecting error
- Assuming produce() return type is unrestricted
Ready to prove it?
Pass the test and this pattern moves unknown
