31

Kotlin Generics Tutorial: Getting Started | raywenderlich.com

 4 years ago
source link: https://www.raywenderlich.com/3634394-kotlin-generics-tutorial-getting-started
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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>.

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!

raywenderlich.com Weekly

The raywenderlich.com newsletter is the easiest way to stay up-to-date on everything you need to know as a mobile developer.

Get a weekly digest of our tutorials and courses, and receive a free in-depth email course as a bonus!

Average Rating

4.8/5

Add a rating for this content

Sign in to add a rating
10 ratings

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK