Kotlin Bootcamp for Programmers 5.2: Generics

1. Welcome

This codelab is part of the Kotlin Bootcamp for Programmers course. You'll get the most value out of this course if you work through the codelabs in sequence. Depending on your knowledge, you may be able to skim some sections. This course is geared towards programmers who know an object-oriented language, and want to learn Kotlin.

sEioGm-YJlcEfGjX0S6M-MQDi23k2ZjQCNPkuImT4e5BIqCJ7XCoLqvDJlUK4cB9XfffJQOcpcW_I8J1LRpYN6qk_b7NMWSQi_0yAWk6Gm5e9C-vvNo5v8geG9iINqKPc_byPxgqMA

Introduction

In this codelab you are introduced to generic classes, functions, and methods, and how they work in Kotlin.

Rather than build a single sample app, the lessons in this course are designed to build your knowledge, but be semi-independent of each other so you can skim sections you're familiar with. To tie them together, many of the examples use an aquarium theme. And if you want to see the full aquarium story, check out the Kotlin Bootcamp for Programmers Udacity course.

BLgezynJ_92kR7lbZPbmkh7cDUCFMm3Ugo_JUOdDd5IpMdkk8nu3nbMiSkQWK5dx4-NX4qlbUXwU9l_Pj_7QqoRSUX2YbiddIUO9I100elofv-IY6xAHo7RL9CCXnjEwBKyLknPHzw

What you should already know

  • The syntax of Kotlin functions, classes, and methods
  • How to create a new class in the IntelliJ IDEA and run a program

What you'll learn

  • How to work with generic classes, methods, and functions

What you'll do

  • Create a generic class and add constraints
  • Create in and out types
  • Create generic functions, methods, and extension functions

2. Task: Explore generic classes

Introduction to generics

Kotlin, like many programming languages, has generic types. A generic type allows you to make a class generic, and thereby make a class much more flexible.

Imagine you were implementing a MyList class that holds a list of items. Without generics, you would need to implement a new version of MyList for each type: one for Double, one for String, one for Fish. With generics, you can make the list generic, so it can hold any type of object. It's like making the type a wildcard that will fit many types.

To define a generic type, put T in angle brackets <T> after the class name. (You could use another letter or a longer name, but the convention for a generic type is T.)

class MyList<T> {
    fun get(pos: Int): T {
        TODO("implement")
    }
    fun addItem(item: T) {}
}

You can reference T as if it were a normal type. The return type for get() is T, and the parameter to addItem() is of type T. Of course, generic lists are very useful, so the List class is built into Kotlin.

Step 1: Make a type hierarchy

In this step you create some classes to use in the next step. Subclassing was covered in an earlier codelab, but here is a brief review.

  1. To keep the example uncluttered, create a new package under src and call it generics.
  2. In the generics package, create a new Aquarium.kt file. This allows you to redefine things using the same names without conflicts, so the rest of your code for this codelab goes into this file.
  3. Make a type hierarchy of water supply types. Start by making WaterSupply an open class, so it can be subclassed.
  4. Add a boolean var parameter, needsProcessing. This automatically creates a mutable property, along with a getter and setter.
  5. Make a subclass TapWater that extends WaterSupply, and pass true for needsProcessing, because the tap water contains additives which are bad for fish.
  6. In TapWater, define a function called addChemicalCleaners() that sets needsProcessing to false after cleaning the water. The needsProcessing property can be set from TapWater, because it is public by default and accessible to subclasses. Here is the completed code.
package generics

open class WaterSupply(var needsProcessing: Boolean)

class TapWater : WaterSupply(true) {
   fun addChemicalCleaners() {
       needsProcessing = false
   }
}
  1. Create two more subclasses of WaterSupply, called FishStoreWater and LakeWater. FishStoreWater doesn't need processing, but LakeWater must be filtered with the filter() method. After filtering, it does not need to be processed again, so in filter(), set needsProcessing = false.
class FishStoreWater : WaterSupply(false)

class LakeWater : WaterSupply(true) {
   fun filter() {
       needsProcessing = false
   }
}

If you need additional information, review the earlier lesson on inheritance in Kotlin.

Step 2: Make a generic class

In this step you modify the Aquarium class to support different types of water supplies.

  1. In Aquarium.kt, define an Aquarium class, with <T> in brackets after the class name.
  2. Add an immutable property waterSupply of type T to Aquarium.
class Aquarium<T>(val waterSupply: T)
  1. Write a function called genericsExample(). This isn't part of a class, so it can go at the top level of the file, like the main() function or the class definitions. In the function, make an Aquarium and pass it a WaterSupply. Since the waterSupply parameter is generic, you must specify the type in angle brackets <>.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
}
  1. In genericsExample() your code can access the aquarium's waterSupply. Because it is of type TapWater, you can call addChemicalCleaners() without any type casts.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. When creating the Aquarium object, you can remove the angle brackets and what's between them because Kotlin has type inference. So there's no reason to say TapWater twice when you create the instance. The type can be inferred by the argument to Aquarium; it will still make an Aquarium of type TapWater.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    aquarium.waterSupply.addChemicalCleaners()
}
  1. To see what is happening, print needsProcessing before and after calling addChemicalCleaners(). Below is the completed function.
fun genericsExample() {
    val aquarium = Aquarium<TapWater>(TapWater())
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
    aquarium.waterSupply.addChemicalCleaners()
    println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}
  1. Add a main() function to call genericsExample(), then run your program and observe the result.
fun main() {
    genericsExample()
}
⇒ water needs processing: true
water needs processing: false

Step 3: Make it more specific

Generic means you can pass almost anything, and sometimes that's a problem. In this step you make the Aquarium class more specific about what you can put in it.

  1. In genericsExample(), create an Aquarium, passing a string for the waterSupply, then print the aquarium's waterSupply property.
fun genericsExample() {
    val aquarium2 = Aquarium("string")
    println(aquarium2.waterSupply)
}
  1. Run your program observe the result.
⇒ string

The result is the string you passed, because Aquarium doesn't put any limitations on T.Any type, including String, can be passed in.

  1. In genericsExample(), create another Aquarium, passing null for the waterSupply. If the waterSupply is null, print "waterSupply is null".
fun genericsExample() {
    val aquarium3 = Aquarium(null)
    if (aquarium3.waterSupply == null) {
        println("waterSupply is null")
    }
}
  1. Run your program and observe the result.
⇒ waterSupply is null

Why can you pass null when creating an Aquarium? This is possible because by default, T stands for the nullable Any? type, the type at the top of the type hierarchy. The following is equivalent to what you typed earlier.

class Aquarium<T: Any?>(val waterSupply: T)
  1. To not allow passing null, make T of type Any explicitly, by removing the ? after Any.
class Aquarium<T: Any>(val waterSupply: T)

In this context, Any is called a generic constraint. It means any type can be passed for T as long as it isn't null.

  1. What you really want is to make sure that only a WaterSupply (or one of its subclasses) can be passed for T. Replace Any with WaterSupply to define a more specific generic constraint.
class Aquarium<T: WaterSupply>(val waterSupply: T)

Step 4: Add more checking

In this step you learn about the check() function to help ensure your code is behaving as expected. The check()function is a standard library function in Kotlin. It acts as an assertion and will throw an IllegalStateException if its argument evaluates to false.

  1. Add an addWater() method to Aquarium class to add water, with a check() that makes sure you don't need to process the water first.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
    fun addWater() {
        check(!waterSupply.needsProcessing) { "water supply needs processing first" }
        println("adding water from $waterSupply")
    }    
}

In this case, if needsProcessing is true, check() will throw an exception.

  1. In genericsExample(), add code to make an Aquarium with LakeWater, and then add some water to it.
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.addWater()
}
  1. Run your program, and you will get an exception, because the water needs to be filtered first.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
        at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)
  1. Add a call to filter the water before adding it to the Aquarium. Now when you run your program, there is no exception thrown.
fun genericsExample() {
    val aquarium4 = Aquarium(LakeWater())
    aquarium4.waterSupply.filter()
    aquarium4.addWater()
}
⇒ adding water from generics.LakeWater@880ec60

The above covers the basics of generics. The following tasks cover more, but the important concept is how to declare and use a generic class with a generic constraint.

3. Task: Learn about in and out types

In this task, you learn about in and out types with generics. An in type is a type that can only be passed into a class, not returned. An out type is a type that can only be returned from a class.

Look at the Aquarium class and you'll see that the generic type is only ever returned when getting the property waterSupply. There aren't any methods that take a value of type T as a parameter (except for defining it in the constructor). Kotlin lets you define out types for exactly this case, and it can infer extra information about where the types are safe to use. Similarly, you can define in types for generic types that are only ever passed into methods, not returned. This allows Kotlin to do extra checks for code safety.

The in and out types are directives for Kotlin's type system. Explaining the whole type system is outside the scope of this bootcamp (it's pretty involved); however, the compiler will flag types that are not marked in and out appropriately, so you need to know about them.

Step 1: Define an out type

  1. In the Aquarium class, change T: WaterSupply to be an out type.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    ...
}
  1. In the same file, outside the class, declare a function addItemTo() that expects an Aquarium of WaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
  1. Call addItemTo() from genericsExample() and run your program.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    addItemTo(aquarium)
}
⇒ item added

Kotlin can ensure that addItemTo() won't do anything type unsafe with the generic WaterSupply, because it's declared as an out type.

  1. If you remove the out keyword, the compiler will give an error when calling addItemTo(), because Kotlin can't ensure that you are not doing anything unsafe with the type. bea96c26eaed641c.png

Step 2: Define an in type

The in type is similar to the out type, but for generic types that are only ever passed into functions, not returned. If you try to return an in type, you'll get a compiler error. In this example you'll define an in type as part of an interface.

  1. In Aquarium.kt, define an interface Cleaner that takes a generic T that's constrained to WaterSupply. Since it is only used as an argument to clean(), you can make it an in parameter.
interface Cleaner<in T: WaterSupply> {
    fun clean(waterSupply: T)
}
  1. To use the Cleaner interface, create a class TapWaterCleaner that implements Cleaner for cleaning TapWater by adding chemicals.
class TapWaterCleaner : Cleaner<TapWater> {
    override fun clean(waterSupply: TapWater) =   waterSupply.addChemicalCleaners()
}
  1. In the Aquarium class, update addWater() to take a Cleaner of type T, and clean the water before adding it.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
    fun addWater(cleaner: Cleaner<T>) {
        if (waterSupply.needsProcessing) {
            cleaner.clean(waterSupply)
        }
        println("water added")
    }
}
  1. Update the genericsExample() example code to make a TapWaterCleaner, an Aquarium with TapWater, and then add some water using the cleaner. It will use the cleaner as needed.
fun genericsExample() {
    val cleaner = TapWaterCleaner()
    val aquarium = Aquarium(TapWater())
    aquarium.addWater(cleaner)
}

Kotlin will use the in and out type information to make sure your code uses the generics safely. Out and in are easy to remember: out types can be passed outward as return values, in types can be passed inward as arguments.

29c11b278d60a7f0.png

If you want to dig in more to the sort of problems in types and out types solve, the documentation covers them in depth.

4. Task: Find out about generic functions

In this task you will learn about generic functions and when to use them. Typically, making a generic function is a good idea whenever the function takes an argument of a class that has a generic type.

Step 1: Make a generic function

  1. In generics/Aquarium.kt, make a function isWaterClean() which takes an Aquarium. You need to specify the generic type of the parameter; one option is to use WaterSupply.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
   println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}

But this means Aquarium must have an out type parameter for this to be called. Sometimes out or in is too restrictive because you need to use a type for both input and output. You can remove the out requirement by making the function generic.

  1. To make the function generic, put angle brackets after the keyword fun with a generic type T and any constraints, in this case, WaterSupply. Change Aquarium to be constrained by T instead of by WaterSupply.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
   println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}

T is a type parameter to isWaterClean() that is being used to specify the generic type of the aquarium. This pattern is really common, and it's a good idea to take a moment to work through this.

  1. Call the isWaterClean() function by specifying the type in angle brackets right after the function name and before the parentheses.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean<TapWater>(aquarium)
}
  1. Because of type inference from the argument aquarium, the type isn't needed, so remove it. Run your program and observe the output.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    isWaterClean(aquarium)
}
⇒ aquarium water is clean: false

Step 2: Make a generic method with a reified type

You can use generic functions for methods too, even in classes that have their own generic type. In this step, you add a generic method to Aquarium that checks if it has a type of WaterSupply.

  1. In Aquarium class, declare a method, hasWaterSupplyOfType() that takes a generic parameter R (T is already used) constrained to WaterSupply, and returns true if waterSupply is of type R. This is like the function you declared earlier, but inside the Aquarium class.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
  1. Notice that the final R is underlined in red. Hold the pointer over it to see what the error is. e270f4047d18603e.png
  2. To do an is check, you need to tell Kotlin that the type is reified, or real, and can be used in the function. To do that, put inline in front of the fun keyword, and reified in in front of the generic type R.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R

Once a type is reified, you can use it like a normal type—because it is a real type after inlining. That means you can do is checks using the type.

If you don't use reified here, the type won't be "real" enough for Kotlin to allow is checks. That's because non-reified types are only available at compile time, and can't be used at runtime by your program. This is discussed more in the next section.

  1. Pass TapWater as the type. Like calling generic functions, call generic methods by using angle brackets with the type after the function name. Run your program and observe the result.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())   // true
}
⇒ true

Step 3: Make extension functions

You can use reified types for regular functions and extension functions, too.

  1. Outside the Aquarium class, define an extension function on WaterSupply called isOfType() that checks if the passed WaterSupply is of a specific type, for example, TapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T
  1. Call the extension function just like a method.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.waterSupply.isOfType<TapWater>())  
}
⇒ true

With these extension functions, it doesn't matter what type of Aquarium it is (Aquarium or TowerTank or some other subclass), as long as it is an Aquarium. Using the star-projection syntax is a convenient way to specify a variety of matches. And when you use a star-projection, Kotlin will make sure you don't do anything unsafe, too.

  1. To use a star-projection, put <*> after Aquarium. Move hasWaterSupplyOfType() to be an extension function, because it isn't really part of the core API of Aquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R
  1. Change the call to hasWaterSupplyOfType() and run your program.
fun genericsExample() {
    val aquarium = Aquarium(TapWater())
    println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true

5. Concept: Reified types and type erasure

In the earlier example, you had to mark the generic type as reified and make the function inline, because Kotlin needs to know about them at runtime, not just compile time.

All generic types are only used at compile time by Kotlin. This lets the compiler make sure that you're doing everything safely. By runtime all the generic types are erased, hence the earlier error message about checking an erased type.

It turns out the compiler can create correct code without keeping the generic types until runtime. But it does mean that sometimes you do something, like is checks on generic types, that the compiler can't support. That's why Kotlin added reified, or real, types.

You can read more about reified types and type erasure in the Kotlin documentation.

6. Summary

This lesson focused on generics, which are important for making code more flexible and easier to reuse.

  • Create generic classes to make code more flexible.
  • Add generic constraints to limit the types used with generics.
  • Use in and out types with generics to provide better type checking to restrict types being passed into or returned from classes.
  • Create generic functions and methods to work with generic types. For example: fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... }
  • Use generic extension functions to add non-core functionality to a class.
  • Reified types are sometimes necessary because of type erasure. Reified types, unlike generic types, persist to runtime.
  • Use the check() function to verify your code is running as expected. For example: check(!waterSupply.needsProcessing) { "water supply needs processing first" }

7. Learn more

Kotlin documentation

If you want more information on any topic in this course, or if you get stuck, https://kotlinlang.org is your best starting point.

Kotlin tutorials

The https://play.kotlinlang.org website includes rich tutorials called Kotlin Koans, a web-based interpreter, and a complete set of reference documentation with examples.

Udacity course

To view the Udacity course on this topic, see Kotlin Bootcamp for Programmers.

IntelliJ IDEA

Documentation for the IntelliJ IDEA can be found on the JetBrains website.

8. Homework

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

  • Assign homework if required.
  • Communicate to students how to submit homework assignments.
  • Grade the homework assignments.

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Answer these questions

Question 1

Which of the following is the convention for naming a generic type?

<Gen>

<Generic>

<T>

<X>

Question 2

A restriction on the types allowed for a generic type is called:

▢ a generic restriction

▢ a generic constraint

▢ disambiguation

▢ a generic type limit

Question 3

Reified means:

▢ The real execution impact of an object has been calculated.

▢ A restricted entry index has been set on the class.

▢ The generic type parameter has been made into a real type.

▢ A remote error indicator has been triggered.

9. Next codelab

Proceed to the next lesson:

For an overview of the course, including links to other codelabs, see "Kotlin Bootcamp for Programmers: Welcome to the course."