Variance (in & out) in Kotlin Explained

Take a look at how in
and out
keywords work in Kotlin. They’re a way to limit the usage of scenarios when the specified type can be used.
Contrary to how it works in Java, where variance is at the use-site, they’re used in a declaration-site (where the class is being declared). This simplifies types and makes the whole concept easier to understand.
1. Contravariant <in T>
In short in
keyword means that the T
can only be consumed and never produced.
If you have a generic interface
that has a T
only as its input, which the subclasses will have to consume and the T
is never used as a return type, then use in
keyword. Here’s an example:
interface InExample<in T> {
fun consume(input: T)
}
// Usage
class StringInExample : InExample<String> {
override fun consume(input: String) {
println(input)
}
}
fun main() {
val example: InExample<String> = StringInExample()
example.consume("Just a string") // Just a string
}
InExample
limits the return type to T
meaning InExample
cannot have it as one of its output parameters. The following code throws a compilation error:
interface InExample<in T> {
fun consume(input: T)
// Type parameter T is declared as 'in'
// but occurs in 'out' position in type T
fun returnConsumed(): T
}
2. Covariant <out T>
In short out
keyword means that the T
can only be produced and never consumed (the opposite of in
keyword).
If you have a generic interface
that only returns a generic type T
and never has it as its input, then you should use contravariant with out
the keyword. Here’s an example:
interface OutExample<out T> {
fun returnValue(): T
}
// Usage
class StringOutExample : OutExample<String> {
override fun returnValue() = "Just a string"
}
fun main() {
val example: OutExample<String> = StringOutExample()
println(example.returnValue()) // Just a string
}
OutExample
limits the return type to T
meaning OutExample
cannot have it as one of its input parameters. The following code throws a compilation error:
interface OutExample<out T> {
// Type parameter T is declared as 'out'
// but occurs in 'in' position in type T
fun processValue(input: T)
fun returnValue(): T
}
Abstract Factory Pattern
This can be very useful when working with an Abstract Factory design pattern:
interface DungeonFactory<out W : Wall, out F : Floor> {
fun createWall(): W
fun createFloor(): F
}
class CastleDungeonFactory : DungeonFactory<CastleWall, CastleFloor> {
override fun createWall(): CastleWall = CastleWall()
override fun createFloor(): CastleFloor = CastleFloor()
}
This way, you can easily limit the produced types while keeping the code readable.
Thanks for reading! Please follow me and clap if you’ve learned something new!