Kotlin Generics Tutorial: Getting Started

In this tutorial, you’ll become familiar with Kotlin generics so that you can include them in your developments to make your code more concise and flexible. By Pablo L. Sordo Martinez.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 2 of this article. Click here to view the first page.

Covariance and Contravariance

Fortunately, Kotlin (among other languages) offers a few alternatives to make your code more flexible, allowing relationships like Cage<Dog> being a sub-type of Cage<T: Animal>, for instance.

The main idea is that language limitations on this topic arise when trying to read and modify generic data defined in a type. The solution proposed is constraining this read/write access to only allow one of them. In other words, the only reason why the compiler does not permit the assignation cageAnimal = cageDog, is to avoid a situation where the developer decides to modify (write) the value cageAnimal.animal. What if we could forbid this operation so that this generic class would be read-only?

Declaration-Site Variance

Try the following class based on the zoo/exotic-pet shop example in your playground:

class CovariantCage<out T : Animal>(private val t: T?) {
    fun getId(): Int? = t?.id
    fun getName(): String? = t?.name
    fun getContentType(): T? = t?.let { t } ?: run { null }
    fun printAnimalInfo(): String = "Animal ${t?.id} is called ${t?.name}"
}

As you can see, there is an unknown term: out. This Kotlin reserved keyword indicates that T is only going to be produced by the methods of this class. Thus, it must only appear in out positions. For this reason, T is the return type of the function getContentType(), for example. None of the other methods include T as an input argument either. This makes CovariantCage covariant in the parameter T, allowing you to add the following assignation:

val dog: Dog = Dog(id = 1, name = "Stu", furColor = FurColor.PATCHED)
var cage1: CovariantCage<Dog> = CovariantCage(dog)
var cage2: CovariantCage<Animal> = cage1   // thanks to being 'out'

By making CovariantCage<Dog> extend from CovariantCage<Animal>, in run-time, any valid type (Animal, Dog, Bird, etc.) will replace T, so type-safety is guaranteed.

On the other hand, there is contravariance. The Kotlin reserved keyword in indicates that class methods will only consume a certain parameter T, not producing it at all. Try this class in your playground:

class ContravariantCage<in T : Bird>(private var t: T?) {
    fun getId(): Int? = t?.id
    fun getName(): String? = t?.name
    fun setContentType(t: T) { this.t = t }
    fun printAnimalInfo(): String = "Animal ${t?.id} is called ${t?.name}"
}

Here, setContentType replaces getContentType from the previous snippet. Thus, this class always consumes T. Therefore, the parameter T takes only in positions in the class methods. This constraint leads to state, for example, that ContravarianceCage<Animal> is a sub-type of ContravarianceCage<Bird>.

Further information is available in the Kotlin official documentation.

Type Projection

The other alternative that Kotlin offers is type projection, which is a materialization of use-site variance.

The idea behind this is indicating a variance constraint at the precise moment in which you use a parameterized class, not when you declare it. For instance, add this class and method to try:

class Cage<T : Animal>(val animal: T, val size: Double)
...
fun examine(cageItem: Cage<out Animal>) {
    val animal: Animal = cageItem.animal
    println(animal)
}

And then, when using it:

val bird: Bird = Eagle(id = 7, name = "Piti", featherColor = FeatherColor.YELLOW, maxSpeed = 75.0f)
val animal: Animal = Dog(id = 1, name = "Stu", furColor = FurColor.PATCHED)
val cage01: Cage<Animal> = Cage(animal = animal, size = 3.1)
val cage02: Cage<Bird> = Cage(animal = bird, size = 0.9)
examine(cage01)
examine(cage02)   // 'out' provides type-safety so that this statement is valid

Generics in Real Scenarios

Reaching this point, you may wonder in which scenarios are generics worth using. In other words, when is it convenient to put what you’ve learned here in action?

Let’s try to implement an interface to manage the zoo/exotic-pet shop. Recall:

class Cage<T : Animal>(val animal: T, val size: Double)

Create the generic interface declaration:

interface Repository<S : Cage<Animal>> {
    fun registerCage(cage: S): Unit
}

Obviously, the following implementation is definitely valid:

class AnimalRepository : Repository<Cage<Animal>> {
    override fun registerCage(cage: Cage<Animal>) {
        println("registering cage for: ${cage.animal.name}")
    }
}

However, this other is not according to the IDE:

class BirdRepository: Repository<Cage<Bird>> {
    override fun registerCage(cage: Cage<Bird>) {
        println("registering cage for: ${cage.animal.name}")
    }
}

The reason is that Repository expects an argument S of type Cage<Animal> or a child of it. By default, the latter does not apply to Cage<Bird>. Fortunately, there is an easy solution which consists of using declaration-site variance on Cage, so that:

class Cage<out T : Animal>(val animal: T, val size: Double)

This new condition also brings a limitation to Cage, since it will never include a function having T as an input argument. For example:

fun sampleFun(t: T) {
    println("dummy behavior")
}

As a rule of thumb, you should use out T in classes and methods that will not modify T, but produce or use it as an output type. Contrary, you should use in T in classes and methods that will consume T, i.e. using it as an input type. Following this rule will buy you type-safety when establishing class hierarchy sub-typing.

Bonus Track: Collections

Apart from the above explanation and examples, it is rather common to get snippets about lists and collections when talking about generics. In general, List is a type easy to implement and understand.

Try out the following example:

var list0: MutableList<Animal>
val list1: MutableList<Dog> = mutableListOf(dog2)
list0 = list1   // the IDE reports an error

The error reported, as you can imagine, relates to MutableList<Dog> not extending from MutableList<Animal>. However, this assignation is OK if you ensure modifications won’t happen in this collections, i.e.:

var list1: MutableList<out Animal>

A similar explanation applies to the contravariant case.

Where to Go From Here?

You can download the final version of the playground with all of these examples using the Download Materials button at the top or bottom of the tutorial.

While there are several good references for Kotlin generics, the best source may be the official documentation. It is rather concise and comes with a good number of well-documented examples.

Perhaps the best thing you can do after reading this article is to jump straight into your code and work it out. Have a look at the tips provided and use them to make your applications more flexible and re-usable.

I hope you enjoyed this tutorial about Kotlin generics. If you have any questions or comments, please join the forum discussion below!