📖 Scenario: Imagine you are building a simple system to handle different types of fruits and their baskets. You want to learn how Kotlin's in keyword helps with contravariance, allowing you to safely use a basket that can accept fruits or their supertypes.
🎯 Goal: You will create a generic interface with contravariance using in, then implement it and use it to add fruits to baskets safely.
📋 What You'll Learn
Create a generic interface called Basket with contravariance using in
Create classes Fruit and Apple where Apple inherits from Fruit
Implement the Basket interface in a class called FruitBasket
Use the contravariant Basket type to add an Apple to a basket of Fruit
Print confirmation messages showing the fruit added
💡 Why This Matters
🌍 Real World
Contravariance is useful when you want to write flexible code that can accept inputs of a more general type, such as event handlers or consumers in UI programming.
💼 Career
Understanding variance helps you work with Kotlin generics safely and effectively, a skill important for Android development and backend Kotlin projects.
Progress0 / 4 steps
1
Create the Fruit and Apple classes
Create a class called Fruit and a class called Apple that inherits from Fruit.
Kotlin
Hint
Use open class Fruit to allow inheritance, then class Apple : Fruit() to inherit.
2
Create the contravariant Basket interface
Create a generic interface called Basket with a contravariant type parameter T using in. Add a function addItem that takes a parameter of type T.
Kotlin
Hint
Use interface Basket to declare contravariance and define fun addItem(item: T).
3
Implement FruitBasket class
Create a class called FruitBasket that implements Basket<Fruit>. Inside, override addItem to print "Added a fruit to the basket".
Kotlin
Hint
Implement Basket<Fruit> and override addItem to print the message.
4
Use contravariance to add an Apple to FruitBasket
Create a variable basket of type Basket<Apple> and assign it a FruitBasket(). Then call basket.addItem(Apple()). Print the output.
Kotlin
Hint
Assign FruitBasket() to Basket<Apple> variable and call addItem(Apple()).
Practice
(1/5)
1. What does the in keyword mean when used in Kotlin generics?
easy
A. It marks a generic type as contravariant, allowing it to accept supertypes safely.
B. It marks a generic type as covariant, allowing it to produce subtypes safely.
C. It restricts a generic type to only accept exact types, no subtypes or supertypes.
D. It means the generic type can be used both as input and output without restrictions.
Solution
Step 1: Understand variance keywords in Kotlin generics
Kotlin uses in for contravariance and out for covariance.
Step 2: Identify the meaning of in
in means 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 A
Quick Check:
in = contravariant = accept supertypes [OK]
Hint: Remember: 'in' means input only, contravariant [OK]
Common Mistakes:
Confusing 'in' with 'out' variance
Thinking 'in' allows output of the generic type
Assuming 'in' restricts to exact types only
2. Which of the following is the correct syntax to declare a contravariant generic interface in Kotlin?
easy
A. interface Consumer { fun consume(item: T) }
B. interface Consumer { fun consume(item: T) }
C. interface Consumer<T> { fun consume(item: in T) }
D. interface Consumer<T> { fun consume(item: out T) }
Solution
Step 1: Recall syntax for contravariant generic declaration
Contravariance uses in before 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 B
Quick Check:
Contravariant syntax = in before T [OK]
Hint: Use 'in' before generic type name for contravariance [OK]
Common Mistakes:
Using 'out' instead of 'in' for contravariance
Placing 'in' inside method parameters incorrectly
Omitting variance keyword entirely
3. Consider the following Kotlin code:
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?
medium
A. Prints 'Consumed: 42' successfully.
B. Runtime error due to type mismatch.
C. Compilation error because Consumer<Int> is not a subtype of Consumer<Number>.
D. Prints nothing because consume is never called.
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 C
Quick Check:
Contravariance reverses subtype relation [OK]
Hint: Contravariant: Consumer<Super> accepts Consumer<Sub>, not vice versa [OK]
Common Mistakes:
Assuming Consumer<Int> can be passed where Consumer<Number> is expected
Confusing covariance and contravariance
Expecting runtime error instead of compile-time error
4. Given this Kotlin code snippet:
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?
medium
A. Because Consumer<Double> cannot be passed where Consumer<Number> is expected due to contravariance.
B. Because Double is not a subtype of Number.
C. Because consume method parameter type is incorrect.
D. Because process function tries to consume an Int instead of a Double.
Solution
Step 1: Recall contravariance subtype rules
For Consumer, 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 A
Quick Check:
Contravariance reverses subtype relation [OK]
Hint: Contravariant types reverse subtype order [OK]
Common Mistakes:
Thinking Double is not subtype of Number
Confusing method parameter types
Assuming runtime error instead of compile-time
5. You want to create a function that accepts any consumer that can consume 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)
}
hard
A. Nothing
B. Number
C. Int
D. Any
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>), use Any as the generic parameter.
Final Answer:
Any -> Option D
Quick Check:
Contravariant consumer accepts supertypes [OK]
Hint: Use Any (a supertype of Number) for contravariant parameter to accept consumers of Number or its supertypes [OK]
Common Mistakes:
Using supertype like Any, which only accepts Consumer<Any>
Using subtype like Int
Using Nothing which is subtype of all types
Recommended
Ready to prove it?
Pass the test and this pattern moves unknownstrongwhile earning +1 Grip toward your certificate.