1. Previous: Chapter 17
  2. Next: Chapter 19
Kotlin: An Illustrated Guide • Chapter 18

Generics

Chapter cover image

All the way back in Chapter 8, when we introduced collections, we saw our first generic type: List<String>. Then we saw more generics in Chapter 9 when we looked at the Pair and Map classes. In order to stay focused on learning about collection types, we glossed over the details about these classes. We’ve put off learning about them for long enough, though! It’s finally time for us to gain a full understanding of generics, so buckle up!

Mugs and Beverages

Jennifer’s bakery café offers delightful, sweet pastries and hot beverages, which you can enjoy at a dainty table or in a cozy chair.

Customers in the bakery café.

As Jennifer’s business has been picking up lately, she reached out to her brother Eric, who has been learning Kotlin, to model her operations for her. Eric decided to start with the beverage menu, which includes light, medium, and dark roast coffees, which customers receive in a ceramic mug. He decided to use enum class to represent the coffee options, and a simple class to represent the mug. He also added a drink() function, which prints a line whenever a customer drinks the coffee.

enum class Coffee { LIGHT_ROAST, MEDIUM_ROAST, DARK_ROAST }
class Mug(val beverage: Coffee)

fun drink(coffee: Coffee) = println("Drinking coffee: $coffee")

Now, whenever a customer orders a coffee, Eric can just instantiate a Mug with the right kind of coffee.

Customer ordering a light roast coffee and receiving his order.
val mug = Mug(Coffee.LIGHT_ROAST)

And when he takes a sip of his coffee, Eric can call the drink() function with the beverage in the mug.

Customer sipping on his coffee while Eric writes Kotlin code.
drink(mug.beverage) // Drinking coffee: LIGHT_ROAST

One day, Jennifer decided that it was time to expand her beverage menu to include hot tea. As with the coffee, Eric chose to model these new tea options as an enum class. He also created an overload of the drink() function that accepts tea.

enum class Tea { GREEN_TEA, BLACK_TEA, RED_TEA }
fun drink(tea: Tea) = println("Drinking tea: $tea")

The Mug class also needed some work. After all, you can’t pass an instance of Tea to a Mug that accepts only Coffee. Eric thought about it. “Well, I guess I can just create another mug class, just for tea.” So he renamed Mug to CoffeeMug, and added another class, named TeaMug.

class CoffeeMug(val coffee: Coffee)
class TeaMug(val tea: Tea)

As he finished typing, Jennifer walked up to him and said, “Have I mentioned that I’m going to be expanding my beverage menu next week? I’ll be adding hot chocolate and apple cider!”

Jennifer announces all of the new hot beverages that she's going to add to the menu.

Eric grimaced. “The more beverages my sister adds to the menu, the more mug classes I’m going to have to create. Things are going to get out of hand quickly. How can I make a single Mug class that can hold any kind of beverage?”

Class diagram for a Beverage abstraction. Coffee Tea interface Beverage

So he thought about how a subtype can be used anywhere that the compiler expects a supertype. “That’s it!” he exclaimed, “I’ll create an interface for the general concept of a beverage, and update the tea and coffee classes so that they’re subtypes!” Having recently learned about sealed types, he decided to make the Beverage interface sealed. He also updated the Mug class so that its property’s type is Beverage. Here’s what he ended up with.

sealed interface Beverage
enum class Tea : Beverage { GREEN_TEA, BLACK_TEA, RED_TEA }
enum class Coffee : Beverage { LIGHT_ROAST, MEDIUM_ROAST, DARK_ROAST }

class Mug(val beverage: Beverage)

After making this change, he was able to create instances of Mug with either Coffee or Tea.

val mugOfCoffee = Mug(Coffee.LIGHT_ROAST)
val mugOfTea = Mug(Tea.BLACK_TEA)

“Huzzah! I’ve got a single mug class that can hold any kind of beverage!” When he ran his Kotlin code, though, he was alarmed to see some compiler errors - the call to the drink() functions no longer worked!

fun drink(coffee: Coffee) = println("Drinking coffee: $coffee")
fun drink(tea: Tea) = println("Drinking tea: $tea")

drink(mugOfCoffee.beverage)
drink(mugOfTea.beverage)
Error

Why isn’t this working? Let’s take a closer look.

Declared Types, Actual Types, and Assignment Compatibility

As we learned back in Chapter 12, objects have more than one type at a time. For example, a Coffee object has a type of Coffee, but it also has a type of Beverage and Any. This means it can be assigned to a variable that is declared with any of those types.

val coffee:   Coffee   = Coffee.MEDIUM_ROAST
val beverage: Beverage = Coffee.MEDIUM_ROAST
val anything: Any      = Coffee.MEDIUM_ROAST

As a result, there can be a difference between the type of the variable and the type of the object inside that variable. This brings up a few distinguishing terms.

  • The type that a variable has been declared with is known as its declared type.
  • The most specific type of the object inside a variable is known as its actual type or its runtime type.
Code showing that the declared type could be Coffee, Beverage, or Any, but the actual type is Coffee in each case. val coffee:   Coffee   = Coffee. MEDIUM_ROAST val beverage: Beverage = Coffee. MEDIUM_ROAST val anything: Any      = Coffee. MEDIUM_ROAST Declared Types Actual Type is Coffee in each case

When we assign an object to a variable, property, or parameter, the actual or runtime type is irrelevant. Only the declared type matters. For example, in the following code listing, the second line fails.

val beverage: Beverage = Coffee.MEDIUM_ROAST
val coffee: Coffee = beverage
Error

Even though, as you and I read this code, we know that the actual type of the object inside the beverage variable at runtime will definitely be Coffee, it’s declared type is Beverage. And since a variable whose type is Beverage could possibly hold objects other than Coffee - it could hold a Tea object, for example - Kotlin won’t let us perform this assignment.

In order for an assignment to succeed, the type of an expression on the right-hand side of the equal sign must be the same as the declared type on the left-hand side, or one of its subtypes.

The type of the expression on the right side must be the type on the left side, or one of its subtypes. val beverage: Beverage = Coffee. MEDIUM_ROAST The type of this expression... ...must be Beverage or one of its subtypes.

Assignment compatibility is the term we use to describe whether the object on the right-hand side can be assigned to the variable’s type on the left-hand side. When it can be assigned, we say that the object is assignment-compatible with that type.

Even though we’ve been talking specifically about literal assignments involving an equal sign, it’s important to note that when we call a function with an argument, we’re effectively assigning an object to the function’s parameter, so all of the same rules apply.

The type of the expression inside the parentheses of a function call must be the same as the parameter type that the function declares, or one of its subtypes. drink (Coffee. MEDIUM_ROAST ) fun drink (coffee: Coffee) = println ( "Drinking coffee: $ coffee " ) The type of this expression... ...must be Coffee or one of its subtypes.

Let’s look at the relevant parts of Eric’s code again.

fun drink(coffee: Coffee) = println("Drinking coffee: $coffee")
fun drink(tea: Tea) = println("Drinking tea: $tea")

class Mug(val beverage: Beverage)

val mugOfCoffee = Mug(Coffee.LIGHT_ROAST)
drink(mugOfCoffee.beverage)
Error

Regardless of the beverage property’s actual type at runtime (e.g., Coffee in this example), it can be assigned neither to a parameter of type Coffee, nor to a parameter of type Tea, because its declared type is Beverage. In other words, beverage is not assignment-compatible with either of the drink() overloads. This is why Eric’s code is failing.

In order for this to compile without errors, he would need to cast the beverage property back to the Coffee type, in order to make it assignment-compatible with one of the drink() overloads.

val mugOfCoffee = Mug(Coffee.LIGHT_ROAST)
drink(mugOfCoffee.beverage as Coffee)

Although this works, this means that every time Eric gets the beverage property off of a Mug, if he needs to assign it to the more specific Coffee or Tea type, he would need to cast it. Every time!

It’d be great if he could have a single Mug class that could work with any kind of Beverage, and avoid the need to cast the beverage property all the time.

Thankfully, this can be solved with generic types!

Introduction to Generic Types

Declaring a Generic Type

For a long time now, we’ve utilized functions to reuse expressions. By calling them with different arguments, we get different results. For example, we created this function to figure out the circumference of a circle back in Listing 2.3.

fun circumference(radius: Double) = 2 * pi * radius

A function with a parameter like this is kind of like a fill-in-the-blank expression.

A fill-in-the-blank expression: 2 * pi * ___ 2 * pi *

Whenever you call it, you give it an argument to put into that blank.

Calling a function is like putting an argument into the blank. 2 * pi * 3.0 circumference ( 3.0 ) 2 * pi * 2.0 circumference ( 2.0 )

So a function’s parameter is basically a blank that you can fill in with a value.

What if there were something similar for _types~? For example, what if the type of the beverage property were a blank that we could fill in with different types?

A fill-in-the-blank class, where you can fill in the type of the beverage property. class Mug( val beverage :       )

Imagine all the different types we could put there!

Many different kinds of hot beverage types could be put in that blank. class Mug( val beverage :       ) Coffee class Mug( val beverage :           ) AppleCider class Mug( val beverage :     ) Tea class Mug( val beverage :              ) HotChocolate

Well, just like functions can have parameters, types can have type parameters.

To add a type parameter to a class, we can put the name of the type parameter inside angle brackets < and >, to the right of the name of the class, like this:

A type parameter is declared within angle brackets to the right of the class name. It can be used in many places where a type is normally used. Declaring a type parameter Using a type parameter class Mug< BEVERAGE_TYPE >( val beverage : BEVERAGE_TYPE )

Here, we named the type parameter BEVERAGE_TYPE, but it’s more typical to give them names that are just one letter long. such as T.

class Mug<T>(val beverage: T)

With this, the Mug class can now be used with different types! Let’s see how to do that next.

Using a Generic Type

When we call a function that has a parameter, we have to supply an argument for that parameter. Likewise, when creating an instance of a Mug, we’ll have to supply a type argument for the type parameter named T. To do this, we can put the type argument in angle brackets next to the name of the class, like this:

A type argument is supplied between angle brackets next to the name of the class. Type argument val mug = Mug<Coffee>(Coffee. LIGHT_ROAST )

Much like calling a function with an argument is effectively the same as replacing the parameter with that value, creating a type with a type argument is a lot like filling in the type parameter with that type.1

Supplying a type argument is much like supplying a function argument. class Mug<Coffee>( val beverage :          ) Coffee Mug<Coffee> 2 * pi * 3.0 circumference ( 3.0 ) Function Argument Type Argument

Even though a type argument must be supplied when constructing a generic class, we usually don’t have to write it out ourselves, because Kotlin can use its type inference to figure it out. For example, since the constructor of Mug takes an argument whose type is T, Kotlin knows that if you create an instance with Mug and pass it a Coffee object, then the type argument for this Mug instance should be Coffee.

Kotlin can often use type inference to figure out the type argument. Because this constructor parameter uses the type parameter T... ... and because we're using a constructor argument whose type is Coffee... val mug = Mug(Coffee. LIGHT_ROAST ) class Mug< T >( val beverage : T ) ... then the inferred type argument here is also Coffee.

Note that the type of this mug variable is not Mug<T>. It’s Mug<Coffee>, and we can explicitly specify its type like this:

val mug: Mug<Coffee> = Mug(Coffee.LIGHT_ROAST)

It’s important to be able to distinguish between Mug<T> and Mug<Coffee>. A class that has a type parameter, such as Mug<T>, is known as a generic type. A generic whose type parameter has been filled with a type argument is known as a parameterized type.

The one generic Mug class can be used to create many different parameterized types. construction Mug<HotChocolate> Mug<AppleCider> Mug<Tea> Mug<Coffee> Mug< T > Parameterized Types Generic Type

The great thing about making the Mug class generic is that the beverage property will retain its specific type.

  • When getting the beverage property from a Mug<Coffee>, its type will be Coffee.
  • When getting the beverage property from a Mug<Tea>, its type will be Tea.

Because of this, there’s no need to cast it when using it with code that expects a specific type! For example, in the following code, we don’t need to cast the beverage property to Tea, because that’s what its type is already. So, the call to drink(tea: Tea) works just fine.

val mug: Mug<Tea> = Mug(Tea.GREEN_TEA)
drink(mug.beverage)

After Eric made all of these changes, the customers were able to enjoy their tea and coffee. He was able to use a single Mug class, and never needed to cast the beverage property.

He was about to discover some surprises with his code, though! Let’s find out what happened next.

Type Parameter Constraints

One day, Jennifer decided to replace all of the café’s ceramic mugs with fancy new temperature-controlled mugs that will keep the customers’ tea and coffee at just the right drinking temperature.

She looked at Eric and said, “The mug will need to set its temperature based on the beverage inside it. If there’s tea in the mug, then it should set the temperature to 140 degrees. If there’s coffee in there, then it should set it to only 135 degrees.”

Eric rolled up his sleeves and got to work. He updated all of the Beverage types, so that coffee and tea can each have its own ideal temperature.

sealed interface Beverage {
    val idealTemperature: Int
}

enum class Tea : Beverage {
    GREEN_TEA, BLACK_TEA, RED_TEA;
    override val idealTemperature = 140
}

enum class Coffee : Beverage {
    LIGHT_ROAST, MEDIUM_ROAST, DARK_ROAST;
    override val idealTemperature = 135
}

Next, in order to represent the temperature-controlled mug, he added a new temperature property to his Mug class, and assigned it to the beverage’s ideal temperature. To his surprise, he ended up with a compile-time error!

class Mug<T>(val beverage: T) {
    val temperature = beverage.idealTemperature
}
Error

It seems that the Mug class is unable to see the new idealTemperature property that he added. As Eric wondered about this, a customer walked up to Jennifer and asked why his mug had a string in it!

Customer complaining that he has a string in his mug.

“A string? It’s supposed to be a beverage!” Jennifer cried. She shot a glance to her brother, who looked at the customer. It was true, there was a string in his mug! Eric looked back down at his computer screen and hammered out some more code. Sure enough, it was possible to put a String in a Mug. In fact, as the Mug code is currently written, literally anything can be stuffed inside it!

val mugOfString: Mug<String> = Mug("How did this get in the mug?")
val mugOfInt: Mug<Int> = Mug(5)
val mugOfBoolean: Mug<Boolean> = Mug(true)
val mugOfEmptiness: Mug<Any?> = Mug(null)

So Eric now had two problems:

  1. The Mug class can’t see the idealTemperature property of its Beverage. (Listing 18.18)
  2. The Mug class can hold an object of any type, but it should only hold a Beverage. (Listing 18.19)

Thankfully, the solution to both of these problems is the same - Eric needs to constrain the type parameter, so that only Beverage types can go inside it.

To do this, he can add a type parameter constraint, which will ensure that the type argument is of a particular type. To do this, after the name of the type parameter, add a colon and the name of the type that will serve as the upper bound for that parameter. For example, to add an upper bound constraint of Beverage, we can write this.

Adding a type parameter constraint to the Mug class. Type parameter constraint class Mug< T : Beverage>( val beverage : T ) { val temperature = beverage . idealTemperature }

With this change, T can only ever be Beverage or one of its subtypes - Tea or Coffee.

Class diagram that shows a variety of types, including what can and cannot go into a mug. String Int Double Number Any Coffee Tea interface Beverage

Using any other type as a type argument will cause a compiler error.

val mugOfString: Mug<String> = Mug("This won't work any more!")
Error

Also, now that Kotlin knows that beverage will be some kind of Beverage, it’s possible to access any properties or functions that the Beverage type includes! Because the Beverage type includes the idealTemperature property, the following code now works.

class Mug<T : Beverage>(val beverage: T) {
    val temperature = beverage.idealTemperature
}

If we don’t specify the upper bound, Kotlin will assume a default of Any?, which means it’ll accept any type, as we saw in Listing 18.19. If we know for sure that only certain types should be used for a type argument, it’s usually a good idea to give it a type parameter constraint so that the compiler will enforce those rules.

Well, we’ve learned a lot about generics from Jennifer’s bakery café. There are more things we can do with generics, though! Let’s take a look at more ways they can be used.

Generics in Practice

Using Type Parameters

So far, we’ve only used a type parameter for a property parameter, but we can use a type parameter almost anywhere that we might normally write a type within the class body. Frequently, they’re used for function parameters and return types, as shown here.

class Dish<T>(private var food: T) {
    fun replaceFood(newFood: T) {
        println("Replacing $food with $newFood")
        food = newFood
    }

    fun getFood(): T = food
}

Generics with Multiple Type Parameters

Our examples so far have only included one type parameter, but it’s possible for a class to have more than one. For example, a combination order could include one type parameters for food and one type parameter for a beverage. When declaring them, just use a different name for each type parameter, and separate them with commas, like this.

class ComboOrder<T : Food, U : Beverage>(val food: T, val beverage: U)

val combo: ComboOrder<Pastry, Tea> = 
    ComboOrder(Pastry.MUFFIN, Tea.GREEN_TEA)

Don’t go crazy with it, though! It’s rare to use more than two or three type parameters in a generic type.2

Generic Interfaces and Super Classes

In addition to classes, interfaces can also be generic. For example, here’s a generic interface with one property.

interface Dish<T> { val food: T }

When implementing this class, we can substitute actual types for the type parameter. For example, we can create a BowlOfSoup class and instance like this.

class BowlOfSoup(override val food: Soup) : Dish<Soup>

val bowlOfSoup: BowlOfSoup = BowlOfSoup(Soup.TOMATO)

Alternatively, the implementing class can itself declare a type parameter, and relay that to the interface, as shown here.

class Bowl<F>(override val food: F) : Dish<F>

val bowlOfSoup: Bowl<Soup> = Bowl(Soup.TOMATO)

Similarly, abstract and open classes can also be generic, and extending them works like you would expect.

abstract class Dish<T>(val food: T)
class BowlOfSoup(food: Soup) : Dish<Soup>(food)
class Bowl<F>(food: F) : Dish<F>(food)

Just be sure to provide a type argument when calling the superclass’ constructor. And again, that type argument can be a type parameter on the subclass, as is the case on the third line in this code listing.

Generic Functions

We’ve seen how both classes and interfaces can be generic, and how their functions can utilize those type parameters. However, functions can also declare their own type parameters. This is especially common for top-level functions - those which are declared outside of a class.

In this case, the type parameter is declared between the fun keyword and the name of the function. For example, we can make a top-level function that wraps the constructor of Mug.

fun <T : Beverage> serve(beverage: T): Mug<T> = Mug(beverage)

When calling this function, we can specify the type argument explicitly by putting it in angle brackets to the right of the function name.

val mug = serve<Coffee>(Coffee.DARK_ROAST)

But again, Kotlin can usually infer the type, in which case you can omit it.

val mug = serve(Coffee.DARK_ROAST)

We can even create generic extension functions where the receiver type is a type parameter. For example, we can change the serve() function into an extension function like this:

fun <T : Beverage> T.pourIntoMug() = Mug(this)
val mug = Coffee.DARK_ROAST.pourIntoMug()

We’ve now got a good understanding of the range of possibilities with generics. Kotlin’s standard library also includes lots of generic types and functions, and we’ve already seen some of them throughout this book. Now that we understand more about what generics are and how they work, let’s review a few of them!

Generics in the Standard Library

List and Set

Back in Chapter 8, we created List and Set collections using functions like listOf() and mutableSetOf(), which are generic functions. As a refresher, here’s how we can use the listOf() function to create a list of menu items.

val menu = listOf("Bagel", "Croissant", "Muffin", "Crumpet")

Much like the serve() function above, the code in the listOf() function ends up calling the constructor of a generic class called ArrayList. The ArrayList class implements the List interface, which is also generic.

Most of the time, we just allow Kotlin’s type inference to fill in the type arguments for us, but we could explicitly specify them like this:

val menu: List<String> = listOf<String>("Bagel", "Croissant", "Muffin", "Crumpet")

The type argument to the listOf() function is String (regardless of whether we explicitly specify it or allow Kotlin to infer the type based on the function’s arguments). The return type of this function depends on the type argument it was called with. Since it was called with a type argument of String, the return type is List<String>.

Pair

Pair is a generic data class that we came across in Chapter 9. It has two type parameters, which determine the type of the two elements that it contains. As you might recall, we can create an instance of Pair by calling its constructor:

val pair = Pair("Crumpet", "Tea")

Again, we use type inference most of the time, but we could explicitly specify the two type arguments like this.

val pair: Pair<String, String> = Pair<String, String>("Crumpet", "Tea")

This listing looks much more intimidating than the previous listing, so it’s usually a good idea to just allow Kotlin’s type inference to do its thing.

The second way to create a Pair is to use the to() infix function, like this:

val pair = "Crumpet" to "Tea"

The to() function is a generic extension function, similar to the pourIntoMug() function we created above, except that it has two type arguments. The to() function exists to make our code read more naturally, so you should never explicitly specify the type arguments when calling it. Just to help demystify the magic, though, this is how you could do that.

val pair = "Crumpet".to<String, String>("Tea")

We’ve seen why generics are helpful, how to create them, how to use them, and a few examples of how they’re used in the standard library. Before we wrap up this chapter, let’s look at some of the trade-offs involved when we use them.

Trade-Offs of Generics

As we’ve seen, generics are a great way to reuse a class or interface, without needing to cast its properties or function results. They come with some trade-offs, though! Here are a few to consider.

Assignment Compatibility of Generic Types

Back in Listing 18.6 we had a version of the Mug class that was not generic. This is what it looked like:

class Mug(val beverage: Beverage)

With this code, any Mug object can be assigned to any Mug variable, regardless of what kind of beverage it holds. For example, we can declare a Mug variable, and assign it a mug of coffee or a mug of tea.

val mugOfCoffee: Mug = Mug(Coffee.DARK_ROAST)
val mugOfTea: Mug = Mug(Tea.RED_TEA)

var mug: Mug = mugOfCoffee
mug = mugOfTea

Now let’s consider a generic version of this class.

class Mug<T : Beverage>(val beverage: T)

When using this class, we’ll end up with parameterized types such as Mug<Coffee> and Mug<Tea>. By default, these parameterized types are not assignment-compatible. For example, it’s not possible to assign an instance of Mug<Tea> to a variable declared with Mug<Coffee>.

val mugOfCoffee: Mug<Coffee> = Mug(Coffee.DARK_ROAST)
val mugOfTea: Mug<Tea> = Mug(Tea.RED_TEA)

var mug: Mug<Coffee> = mugOfCoffee
mug = mugOfTea
Error

This isn’t surprising. What can catch some developers by surprise, though, is that it’s also not possible to assign mugOfCoffee or mugOfTea to a variable that has type Mug<Beverage>.

val mugOfCoffee: Mug<Coffee> = Mug(Coffee.DARK_ROAST)
val mugOfTea: Mug<Tea> = Mug(Tea.RED_TEA)

var mug: Mug<Beverage> = mugOfCoffee
Error

There are some ways that we can work around this, as we’ll see throughout the next chapter! Note that this assignment does actually work when assigning directly from a constructor call.

val mug: Mug<Beverage> = Mug(Coffee.DARK_ROAST)

Type Erasure

Probably the most significant trade-off is called type erasure. Although an object’s type arguments are known at compile-time, they are erased before your code runs. In other words, an object’s type arguments are not known at runtime.

The type inside the angle brackets is known at compile time but not at runtime. Mug<Coffee> This type is known at compile time and at runtime. This type is known at compile time but NOT at runtime.

Let’s look at a few ways that type erasure can affect your code.

Checking the Type of Type Arguments

One consequence of type erasure is that it’s not possible to use is to check the type of a parameterized type’s type argument at runtime. For example, the following code tries to determine whether the mug instance has a type of Mug<Tea> or Mug<Coffee>. This results in a compiler error.

val mug: Mug<Beverage> = Mug(Coffee.MEDIUM_ROAST)

when (mug) {
    is Mug<Tea>    -> println("Sipping on tea: ${mug.beverage}!")
    is Mug<Coffee> -> println("Sipping on coffee: ${mug.beverage}!")
}
Error

However, it is still possible to check the type of a property that was declared with a type parameter (such as beverage), so this works just fine:

val mug: Mug<Beverage> = Mug(Coffee.MEDIUM_ROAST)

when (mug.beverage) {
    is Tea    -> println("Sipping on tea: ${mug.beverage}!")
    is Coffee -> println("Sipping on coffee: ${mug.beverage}!")
}

Function Overloads on JVM

Kotlin code can target different kinds of computer systems and environments. Most often, a Kotlin project targets the Java Virtual Machine (JVM), and if you’ve been following along with the code in this book, that’s probably what you’ve been doing. However, you can also use Kotlin to create programs that run natively on different platforms, like Windows, Linux, Mac, and so on. In fact, you can even create Kotlin code that compiles down to JavaScript!

Once in a while, there’s a limitation that affects some of these platforms, but not others. When it comes to type erasure, Kotlin code that targets the JVM has a limitation that doesn’t affect native or JavaScript targets: it’s not possible to use function overloads where the functions’ parameters differ only on their type arguments. For example, in the following code, the only difference between the signatures of these two functions is the type argument of their parameters. Compiling this code on the JVM produces an error.

Annotated code example showing two functions whose signatures differ only on the type arguments of the function parameters. These two function signatures are the same except for the type argument here... ... and here. fun drinkFrom (mug: Mug<Tea>) = println ( "Drinking tea" ) fun drinkFrom (mug: Mug<Coffee>) = println ( "Drinking coffee" )

Even with these trade-offs, generics are incredibly helpful, so it’s important to know about them!

Summary

Enjoying this book?
Pick up the Leanpub edition today!

Kotlin: An Illustrated Guide is now available on Leanpub See the book on Leanpub

As the seasons changed, so did the menu at Jennifer’s bakery café, but thanks to Eric’s newfound understanding of Kotlin generics, adapting to these changes became a piece of cake! And now that you know all about generics, you’ll be able to adapt to changes, too! Here’s a quick review of what we learned:

As we saw in this chapter, the subtyping of generics doesn’t always work like we expect - Mug<Coffee> is not naturally a subtype of Mug<Beverage>. However, with a few small changes, we can make that happen. Stay tuned for the next chapter where we cover the fascinating topic of generic variance!


  1. Note that the right side of the illustration shows what the “effective” class would be. In other words, by creating a Mug<Coffee> type, it’s as if we had declared another class called Mug<Coffee> whose beverage type is Coffee. However, we don’t actually write that code - by creating the generic class in Listing 18.14, Kotlin has all it needs to create the type for us! ↩︎

  2. Once in a while you might find a library with lots of type parameters in a single generic type. For example, the Arrow functional programming library includes a class called Tuple10 that includes ten type parameters. Most of the time, you shouldn’t need that many in your own code, though. ↩︎

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit