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.

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.
Now, whenever a customer orders a coffee, Eric can just instantiate a Mug
with the right kind of coffee.

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

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

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?”
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.
After making this change, he was able to create instances of Mug
with either Coffee
or 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!
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.
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.
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.
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.
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.
Let’s look at the relevant parts of Eric’s code again.
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.
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.
A function with a parameter like this is kind of like a fill-in-the-blank expression.
Whenever you call it, you give it an argument to put into that blank.
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?
Imagine all the different types we could put there!
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:
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
.
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:
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
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
.
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:
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 great thing about making the Mug
class generic is that the beverage
property will retain its specific type.
- When getting the
beverage
property from aMug<Coffee>
, its type will beCoffee
. - When getting the
beverage
property from aMug<Tea>
, its type will beTea
.
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.
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.
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!
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!

“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!
So Eric now had two problems:
- The
Mug
class can’t see theidealTemperature
property of itsBeverage
. (Listing 18.18) - The
Mug
class can hold an object of any type, but it should only hold aBeverage
. (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.
With this change, T can only ever be Beverage
or one of its subtypes - Tea
or Coffee
.
Using any other type as a type argument will cause a compiler 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.
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.
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.
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.
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.
Alternatively, the implementing class can itself declare a type parameter, and relay that to the interface, as shown here.
Similarly, abstract and open classes can also be generic, and extending them works like you would expect.
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
.
When calling this function, we can specify the type argument explicitly by putting it in angle brackets to the right of the function name.
But again, Kotlin can usually infer the type, in which case you can omit it.
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:
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.
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:
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:
Again, we use type inference most of the time, but we could explicitly specify the two type arguments like this.
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:
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.
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:
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.
Now let’s consider a generic version of this class.
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>
.
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>
.
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.
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.
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.
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:
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.
Even with these trade-offs, generics are incredibly helpful, so it’s important to know about them!
Summary
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:
- The problem that generics solve.
- How to declare and use a generic type.
- How type parameter constraints can ensure that only the right kinds of type arguments are used.
- How to use generic interfaces and superclasses.
- How to declare and use generic functions.
- How generics are used in the standard library.
- Trade-offs of using generics.
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!
-
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 calledMug<Coffee>
whosebeverage
type isCoffee
. 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! ↩︎ -
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. ↩︎