Blog Infos
Author
Published
Topics
Published
Topics
Photo by Gabriele bartoletti stella on Unsplash

Before jumping into variance, let’s quickly take a look at how generic classes look in Kotlin

class Box<T> {
  fun wrap(item: T) {
    println("$item is now wrapped in a box")
  }
}

Here, we have a class Box with a single function, wrapWrap accepts an argument of type T. T represents a generic type. When creating an object of the Box class, we must define what class T represents for that instance. Once a box is created for a particular type, the wrap function will expect the item to be of that class. For example, if T = String:

val stringBox = Box<String>()

Box class effectively becomes

class Box {
  fun wrap(item: String)
}

Similarly, if T = Float

val floatBox = Box<Float>()

Box class evaluates to

class Box {
  fun wrap(item: Float)
}

This is why generic types are an important language feature. It allows us to write functions and classes without being tied to any particular class, yet it provides type safety at run time.

Variance

Now that we know what generic types are and when they can be used, we can look at variance in Kotlin.

Contravariant (in)

Let’s say we have the following classes:

interface Fruit { 
  fun getColor(): String 
} 
 
class Apple: Fruit { 
  override fun getColor(): String { 
    return "Red" 
  } 
}

Now we declare a generic interface Eater which can eat items of type T

interface Eater<T> { 
  fun eat(food: T) {} 
}

With this interface, let’s create a fruit eater

val fruitEater = object: Eater<Fruit> { 
  override fun eat(food: Fruit) { 
    println("Eating ${food.getColor()} color fruit") 
  } 
}

Now, if we try to assign a fruit eater to a variable that expects an apple eater

val appleEater: Eater<Apple> = fruitEater

It will give a compiler error saying Required Apple, found Fruit.

This basically means it was expecting an eater who could eat apples, but we gave it an eater who could eat fruits. This sounds incorrect. If an eater can eat (all) fruits, and an apple is a fruit, it should also be able to eat an apple as well, right?

To fix this, let’s make a small change to the Eater interface

interface Eater<in T> { 
  fun eat(food: T) {} 
}

We add in operator in front of T to mark it as contravariant on type T. This means that type T can only be consumed and not produced in this interface. When the compiler has this information about T, it can infer that we are only consuming variables of type T, so it is safe to use in places where subclasses of T are expected.

Going back to our example, our Eater interface has one function that takes in a parameter of type T. Nowhere do we produce objects of type T. Hence, we can safely mark our interface Eater as contravariant on type T. Eater<Fruit> now means we are only consuming fruits and that makes it compatible with Eater of all Fruit’s subclasses. This makes the following statement compile

val appleEater: Eater<Apple> = fruitEater

Let us try assigning an apple eater to a variable of type fruit eater

val fruitEater: Eater<Fruit> = appleEater

The above statement won’t compile. Since apple eater can only consume apples and not any other fruit, we cannot assign it to a variable of type Eater<Fruit>.

To summarize, if a class or interface is contravariant on type T (marked as <in T>), it means it expects (or consumes) an object of type T. In the example above, interface Eater<in T> means

If T = Apple, it can only consume apples, so it can’t be assigned to a variable that expects a fruit consumer as not all fruits are apples.
If T = Fruit, it can consume any fruit, so it can be assigned to a variable that expects an apple consumer as all apples are fruits.

Job Offers

Job Offers


    Senior Android Developer

    SumUp
    Berlin
    • Full Time
    apply now

    Senior Android Engineer

    Carly Solutions GmbH
    Munich
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Invariant

For this third and final type of variance, let’s consider this interface

interface Basket<T> {
    fun put(item: T)
    fun pick(): T?
}

This interface has two methods, one which returns an item of type T and the other which accepts an input of type T. If we try to create a basket of type Fruits, it would look like

class FruitBasket : Basket<Fruit> {
    private val fruits = mutableListOf<Fruit>()

    override fun put(item: Fruit) {
        fruits.add(item)
    }

    override fun pick(): Fruit? {
        if (fruits.isEmpty()) {
            return null
        }
        return fruits.removeAt(fruits.lastIndex)
    }
}

If we try to assign a variable of type FruitBasket to a variable of type AppleBasket

val appleBasket: Basket<Apple> = FruitBasket()

This does not compile, giving an error Required Basket<Apple>, found FruitBasket. In this case, it cannot be fixed because an apple basket not only consumes apples but produces them as well. A fruit basket can accept (via the put method) apples, but it cannot guarantee to produce just apples as it can produce any fruit.

val fruitBasket: Basket<Fruit> = AppleBasket()

On the flip side, we cannot assign an appleBasket to a variable of type Basket<Fruit> because fruitBasket expects any type of fruit to be added, not just apples.

Since Basket produces and consumes an item of type T, it is invariant on type T. When we specify neither in nor out for a generic type, it is treated as an invariant by default.

If you made it this far, hopefully, variance became a friendlier topic than what it was before. For me, personally, I had read about variance multiple times but this only clicked when I was able to establish a mental model for myself.

In this post, I tried to paint a picture of that mental model. If this did not click for you, I would urge you to think in terms of producers and consumers and build a model which you can relate to. Once that happens, it should become a lot less intimidating.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu