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

Introduction

At some point in your path to becoming a better Android developer you might have found yourself wondering about the following:

  1. I think some features I have developed could be refactored with much less boilerplate.
  2. The code I have written for two different features is way too similar.
  3. When adding a new functionality I find myself repeating code with minor differences.

Any of the above statements lead to the same conclusion: Your development mindset is going the extra mile. You are not interested just in delivering, but in doing it more efficiently. Therefore, this is an excellent moment to introduce Kotlin generics to you.

Note: This tutorial assumes you’re comfortable working in Kotlin. If you need to get started learning Kotlin, check out Kotlin For Android: An Introduction.

Making Your Code More Flexible

Let’s set up an example of a rather common scenario. Begin by downloading the starter project using the Download Materials button at the top or bottom of this tutorial. The project contains PlaygroundData.kt which includs all the data classes available, and MainKotlinPlayground.kt with the main() function you’ll use in order to test your code. Open or paste these files in your favorite Kotlin editor or playground. You can also paste in the starting code and follow along at play.kotlinlang.org.

Imagine you are managing a zoo or an exotic pet shop so that you have to organize and keep a reference of all animal species available. Following an object-oriented programming approach, you will most likely put each animal in a cage and declare several attributes and actions associated. For instance, for a dog, you could do the following. Try this out in your playground:

data class Dog(val id: Int, val name: String, val furColor: FurColor)
class Cage(var dog: Dog, val size: Double)

So you can easily use it as follows:

val dog = Dog(id = 0, name = "Doglin", furColor = FurColor.BLACK)
val cage = Cage(dog = dog, size = 6.0)

So far so good. However, the above sample has several potential issues to take into account:

  • A new redefinition of Cage is necessary if classifying other species, i.e. cats. You could use something like CatCage, for example. However, this looks rather naïve and not convenient, since it introduces code duplication.
  • Certain constraints on class instance creation sound convenient, so that cages only host animals, and no other wrong type.

Parameters to the Rescue

To cope with the previous situation, Kotlin allows you to use parameters for methods and attributes, composing what is known as parametrized classes. This approach improves code flexibility and re-usability, since it allows the developer using the same class blueprint in the context of different types defined by a type parameter T. In the example, update Cage as follows:

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

val dog: Dog = Dog(id = 1, name = "Stu", furColor = FurColor.PATCHED)
val cat: Cat = Cat(id = 4, name = "Peter", eyesColor = EyesColor.GREEN)
val cageDog: Cage<Dog> = Cage(animal = dog, size = 6.0)
val cageCat: Cage<Cat> = Cage(animal = cat, size = 3.0)

Here, you have created the class Cage of T so that Cage of Dog and Cage of Cat are implemented without any code duplication. This is now type safe, great! You have figured out the problem with a rather elegant solution. However, there is an important downside. Generally, when coding, you need to prepare your code for the worst, and that usually has to do with other developers. In the above example, nothing stops one of your colleagues from doing this:

val cageString: Cage<String> = 
    Cage(animal = "This cage hosts a String?", size = 0.1)

The above is syntactically valid, though ridiculous. Fortunately, there is still hope ahead. :]

Class Hierarchy

Two of the main foundations in object-oriented programming, Inheritance and Polymorphism, take a starring role when setting a class hierarchy. This is particularly important when preventing code misuses as described above.

Following the exotic pet shop example, you have the class hierarchy illustrated in the following graph in PlaygroundData.kt:

Class hierarchy

Once settled and in order to avoid conflicts with misleading implementations you can write this code to replace your current Cage code:

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

Great! Another problem solved. Now, Cage only permits data types inheriting from Animal. In short, A Cage can only contain what IS-A Animal.

Note: The previous snippet T : Animal is known in Kotlin as generic constraint. This upper bound is equivalent to the Java’s extends keyword. For further information on this topic, have a look to the official documentation.

Finally, recall polymorphism, which allows classes to have initializations like this:

var animal: Animal = Dog(id = 0, name = "Doglin", furColor = FurColor.BLACK)
var dog: Dog = Dog(id = 1, name = "Stu", furColor = FurColor.PATCHED)

And assignations like this:

animal = dog

In other words, the explicitly indicated variable type grants type-safety when re-assigning values.

Following this reasoning, it makes sense to allege a similar behavior for the aforementioned parametrized classes. Try this in your playground:

var cageAnimal: Cage<Animal>
var cageDog: Cage<Dog> = Cage(animal = dog, size = 3.2)
cageAnimal = cageDog   // error condition

What is going on here? Does this mean that inheritance and polymorphism are not working as usual in Kotlin? Have you found a glitch in The Matrix? Not at all, Kotlin is simply preventing our code from potential future errors.

Generics and Variance

Previous sections intended to put on the table a topic that is often dismissed because of its complexity. However, when digging a bit into the problem, it turns out rather easy.

Let’s assume the last snippet did not report any error, i.e. a Cage<Dog> can get assigned to a Cage<Animal>. Thus:

var cageAnimal: Cage<Animal>
var cageDog: Cage<Dog> = Cage(animal = dog, size = 3.2)
cageAnimal = cageDog   // assume no error this time
// if allowed, the following could apply
val cat: Cat = Cat(id = 2, name = "Tula", eyesColor = EyesColor.YELLOWISH)
cageAnimal.animal = cat   // assigning a Cat to Dog type!
val dog: Dog = cageAnimal.animal   // ClassCastException: a Cat is not a Dog

And this is why Kotlin forbids this sort of relationships. It is a way of guaranteeing stability at runtime. If this wasn’t the case, a Cage<Animal> object would be holding a reference to a Cage<Dog>. Then, a careless developer could try including a Cat into cageAnimal, dismissing the fact that it actually refers to cageDog.

Generally speaking, Variance defines Inheritance relationships of parameterized types. Variance is all about sub-typing. Thus, we can say that Cage is invariant in the parameter T.

Note: Bear in mind that the above error only happens when attemping to write or modify (assignation) Cage<Animal>.