Kotlin is a powerful, modern programming language with robust support for generics. But when dealing with generics, you often run into the concept of variance, which can be tricky to grasp at first. In this post, we’ll break down Kotlin variance in a simple and engaging way, ensuring you fully understand generics and subtyping.
What is Kotlin Variance?
Variance defines how generic types relate to each other in the context of subtyping. Consider this:
open class Animal
class Dog : Animal()
In normal inheritance, Dog
is a subtype of Animal
. But does List<Dog>
automatically become a subtype of List<Animal>
? The answer is no, unless we explicitly declare variance.
Kotlin provides three ways to handle variance:
- Covariance (
out
) - Contravariance (
in
) - Invariance (default behavior)
Let’s explore each of these concepts with examples.
Covariance (out
) – Producer
Covariant types allow a generic type to be a subtype of another generic type when its type parameter is only used as an output (producer). It is declared using the out
keyword.
interface Producer<out T> {
fun produce(): T
}
This means that Producer<Dog>
can be used where Producer<Animal>
is expected:
class DogProducer : Producer<Dog> {
override fun produce(): Dog = Dog()
}
val producer: Producer<Animal> = DogProducer() // Works fine
Why Use out
?
- It ensures type safety.
- Used when a class only returns values of type
T
. - Common in collections like
List<T>
, which can only produce items, not modify them.
Example:
fun feedAnimals(producer: Producer<Animal>) {
val animal: Animal = producer.produce()
println("Feeding ${animal::class.simpleName}") //O/P - Feeding Dog
}
Since Producer<Dog>
is a subtype of Producer<Animal>
, this works without any issues.
Contravariance (in
) – Consumer
Contravariant types allow a generic type to be a supertype of another generic type when its type parameter is only used as an input (consumer). It is declared using the in
keyword.
interface Consumer<in T> {
fun consume(item: T)
}
This means that Consumer<Animal>
can be used where Consumer<Dog>
is expected:
class AnimalConsumer : Consumer<Animal> {
override fun consume(item: Animal) {
println("Consuming ${item::class.simpleName}")
}
}
val consumer: Consumer<Dog> = AnimalConsumer() // Works fine
Why Use in
?
- Again it ensures type safety.
- Used when a class only takes in values of type
T
. - Common in function parameters, like
Comparator<T>
.
Example:
fun addDogsToShelter(consumer: Consumer<Dog>) {
consumer.consume(Dog())
}
Since Consumer<Animal>
is a supertype of Consumer<Dog>
, this works perfectly.
Invariance (Default Behavior: No in
or out
)
By default, generics in Kotlin are invariant, meaning Box<Dog>
is NOT a subtype or supertype of Box<Animal>
, even though Dog
is a subtype of Animal
. Means, they do not support substitution for subtypes or supertypes.
class Box<T>(val item: T)
val dogBox: Box<Dog> = Box(Dog())
val animalBox: Box<Animal> = dogBox // Error: Type mismatch
Why?
Without explicit variance, Kotlin prevents unsafe assignments. If variance is not declared, Kotlin assumes that T
can be both produced and consumed, making it unsafe to assume subtyping.
Star Projection (*
) – When Type is Unknown
Sometimes, you don’t know the exact type parameter but still need to work with a generic class. Kotlin provides star projection (*
) to handle such cases.
Example:
fun printList(list: List<*>) {
for (item in list) {
println(item) // Treated as Any?
}
}
A List<*>
means it could be List<Any>
, List<String>
, List<Int>
, etc., but we cannot modify it because we don’t know the exact type.
Best Practices for Kotlin Variance
- Use
out
when the type is only produced (e.g.,List<T>
). - Use
in
when the type is only consumed (e.g.,Comparator<T>
). - Keep generics invariant unless variance is required.
- Use star projections (
*
) when you don’t know the type but need read access.
Conclusion
Understanding Kotlin Variance is crucial for writing flexible and type-safe code. Covariance (out
) is used for producers, contravariance (in
) for consumers, and invariance when both roles exist. By mastering Kotlin Variance concepts, you can work effectively with generics and subtyping in Kotlin.