We’ve seen how we can implement interfaces and extend classes to create subtypes. However, with generic types, we sometimes want to create a subtype based on the type arguments instead of the base types. Kotlin gives us a few different tools to achieve this, and in this chapter we’ll learn all about them.
In order to understand how this is possible, we’ll first need to understand the principles of variance. Then, we’ll look at three simple rules that explain when one parameterized type can be the subtype of another.
To get started, we’ll take a stroll through the local park, and make a few purchases from some vending machines!
How can we get more subtypes from our generics?
As we learned in Chapter 12, we can substitute a subtype anywhere that a supertype is expected.
When it comes to generics, though, things don’t always work the way we might think. While MutableList<Cow> is a subtype of List<Cow>, it’s not a subtype of MutableList<FarmAnimal>.
With a little bit of magic, though, we can finagle parameterized types into becoming the subtypes that we need them to be. To do that, we first need to learn about covariance and contravariance, so let’s jump in!
Covariance
Parker works for the local Parks & Recreation Department. Since the playground is often full of parents with hungry children, he thought, “Let’s add a vending machine in the pavilion.” So he spoke with Vinnie, who owns a nearby business that sets up and stocks vending machines. “I need a vending machine where kids can put in a coin and get back a _snack~”, said Parker. Vinnie agreed, so they drew up a contract.
Once the paperwork was signed, Vinnie installed the new vending machine at the park, where customers could insert a coin and receive a random snack, like trail mix, a bag of gummy bears, or a candy bar. The next day, when Parker saw a kid insert a nickel and receive a bag of trail mix from the machine, he smiled to himself, knowing all was well.
A few weeks later, Parker saw Vinnie replacing the machine. Parker was a bit nervous about the change, so he spoke to him. “This new machine will still accept a coin and return a snack, right?”
“Yes”, began Vinnie, “Trail mix and gummy bears are in short supply, though, so this vending machine is only going to return a candy bar. That’s still okay, right?”
Parker thought about it, and finally replied, “As long as it accepts a coin and returns a snack. A candy bar is a kind of snack, so that should be fine.” After the new machine was substituted for the old one, a child came up, inserted a coin, and received a candy bar. Parker was glad that the new machine did what it was supposed to.
A few weeks passed. One day, Vinnie was at it again, replacing the machine with yet another one. A few minutes after the installation was complete, an irate father complained to Parker, “What’s wrong with the new vending machine? My hungry kid put in a coin and instead of getting a snack, he got a toy action figure—~he can’t eat a toy!~“
Parker quickly caught up with Vinnie again. When asked about the new machine, Vinnie replied, “Oh, I thought I’d switch out the old vending machine with a new one that returns either a snack or a toy.”
“No, no, no! The contract said the machine would accept a coin and return a snack. Toys are not snacks!” Realizing that this new machine didn’t do what Parker needed, Vinnie agreed to resolve the issue.
As we can see from this story, some substitutions work, but others don’t.
- When Vinnie replaced the original vending machine with one that only returns candy bars, everything was fine, because candy bars are still a kind of snack that kids can eat.
- However, when he replaced it with the machine that returns either a snack or a toy, he broke his contract—the machine no longer returned only snacks.
This brings up some important points for us to explore about substitution in Kotlin. To get started, let’s model the first vending machine that Vinnie provided—the one that accepted a coin and returned a snack.
open class VendingMachine { open fun purchase(money: Coin): Snack = randomSnack() }Although this chapter won’t include definitions for
Snackand its related types, here’s a class diagram showing their type hierarchy.Now let’s model Vinnie’s second vending machine by extending the one above. This one looks almost identical to it, but instead of a random snack, it only ever returns a candy bar.
class CandyBarMachine : VendingMachine() { override fun purchase(money: Coin): Snack = CandyBar() }Any code that expects a
VendingMachinecan work with aCandyBarMachine, because it’s a subtype ofVendingMachine—it still accepts a coin and returns a snack, just likeVendingMachinedoes. Because of this, we can substitute aCandyBarMachinefor aVendingMachine. In other words, we can assign aCandyBarMachineto a variable declared as aVendingMachine.Similarly, we can send it as an argument to a function that expects a
VendingMachine.As we saw in the story, a
CandyBaris a kind ofSnack, so it’s also safe to declare that theCandyBarMachineonly returnsCandyBarobjects. Let’s take the code from Listing 19.2 and update the return type.class CandyBarMachine : VendingMachine() { override fun purchase(money: Coin): CandyBar = CandyBar() }As the story revealed, this only works one way. When Vinnie tried to replace the vending machine with one that returned a product that could be either a snack or a toy, he broke his contract. Kids expect to be able to eat anything that the vending machine provides, but toys aren’t edible!
The same principle holds true in Kotlin—a subclass can’t return a more general type than the one declared in its supertype. For example, if we create a
ToyOrSnackMachinethat tries to return any kind ofProduct, we’ll get a compile-time error.class ToyOrSnackMachine : VendingMachine() { override fun purchase(money: Coin): Product = randomToyOrSnack() }ErrorMost classes and interfaces associate with other types. Associated types can appear in a variety of places—as function parameter types, function return types, property types, and so on. For example, the
VendingMachineclass associates with two other types.
Coin- the type of the parameter inpurchase().Snack- the return type ofpurchase().The
VendingMachineclass can have supertypes and subtypes. Similarly, the associated typesCoinandSnackcan also have their own supertypes and subtypes.The nature of the relationship between the type hierarchy of
VendingMachineand the type hierarchy of its associated types can be described by variance. As we look at the code ofVendingMachineandCandyBarMachineside by side, we can see that relationship emerge.Specifically, we can see that as we make a more specific
VendingMachine, we can return a more specificSnackfrom thepurchase()function.Because they become more specific together, this kind of variance is called covariance, where “co-” is a prefix that means “together”.
When talking about variance, developers normally say it one of these ways:
- “A type is covariant with respect to its return types.”
- “A type is covariant on its return types.”
To sum up this section, just remember that within a subtype (e.g.,
CandyBarMachine) a function can return a more specific type (e.g.,CandyBar) than declared in its supertype, but it cannot return a more general type (e.g.,Product).The adventures of Parker and Vinnie continue, though! Brace yourself—Vinnie is about to try substituting a few more vending machines!
Contravariance
A few weeks later, Parker noticed that Vinnie was replacing the machine yet again. “We’re upgrading our machines so that they accept both coins and bills,” he explained.
Parker scratched his head while he thought about it. “Well, I guess that’s fine. As long as the machine still accepts a coin and returns a snack.”
After the new machine was installed, a kid walked up, inserted a dime, and got a snack from the machine. “Great,” Parker thought to himself. “Everything is still good.”
Things continued well over the next few weeks. Even though this new vending machine had a bill acceptor, none of the kids ever used it, because they only ever had coins.
![]()
Wouldn’t you know it—a few weeks later, Parker saw Vinnie replacing the machine one more time. Parker shrugged it off and continued on his way.
Ten minutes later, though, a young girl was crying next to the snack machine.
“What’s wrong?” he asked her.
“I wanted a snack. I have a nickel, but this machine only accepts a dime!” Parker looked at the new vending machine, and sure enough, it only had a slot for dimes, but not any other kind of coin.
![]()
Parker complained to Vinnie again. “No, no, no! The contract said the machine would accept a coin and return a snack. A nickel is a kind of coin, so the machine still needs to accept it!” Embarrassed, Vinnie put back the previous vending machine.
This story shows again that some substitutions work, and some don’t.
When Vinnie replaced the original vending machine with the one that accepted both coins and bills, everything still worked, because it still accepted coins. The kids only ever had coins on them, so they never actually used the bill acceptor, but there was no harm in the machine having one, as long as it could still accept coins.
However, when Vinnie replaced it with the machine that accepted only dimes, he broke his contract—the machine no longer accepted nickels, quarters, or any other kind of coin.
This illustrates additional points about substitution. Let’s take a look at the original
VendingMachinewe created back in Listing 19.1.open class VendingMachine { open fun purchase(money: Coin): Snack = randomSnack() }As with
Snack, this chapter won’t include any class definitions forCoinand its type hierarchy, but here’s a class diagram showing how they relate.As we saw, it was safe for Vinnie to replace the coin-based vending machine with one that can accept either coins or bills. Likewise, it would be safe for Kotlin to allow a subtype to declare a more general parameter type.
But guess what? If we try changing the parameter type from
CointoMoney, we’ll get a compiler error!class AnyMoneyVendingMachine : VendingMachine() { override fun purchase(money: Money): Snack = randomSnack() }ErrorAgain, this would be perfectly safe for Kotlin’s type system to allow. So why does this cause a compiler error?
As you might recall, Kotlin allows us to overload a function—a class or interface can have multiple functions that have the same name, as long as their parameter types differ. In order to support this feature, Kotlin won’t allow us to change the type of a function parameter in subtypes, as we’re trying to do above.
One way to fix this is to simply remove the
overridekeyword, which means we’ll be overloading the function instead of overriding it.class AnyMoneyVendingMachine : VendingMachine() { fun purchase(money: Money): Snack = randomSnack() }Just keep in mind that when we do this, we end up with two
purchase()functions, each with its own body—one inAnyMoneyVendingMachineand one inVendingMachine.So Kotlin’s overloading feature is getting in the way. Overloading only applies to functions declared with the
funkeyword, though, so to work around this, we can changepurchase()from a function to a property that has a function type, like this.open class VendingMachine { open val purchase: (Coin) -> Snack = { randomSnack() } }With this change, we can rewrite
AnyMoneyVendingMachineto override that property.class AnyMoneyVendingMachine : VendingMachine() { override val purchase: (Coin) -> Snack = { randomSnack() } }And finally, we can replace the
Coinparameter type withMoney.class AnyMoneyVendingMachine : VendingMachine() { override val purchase: (Money) -> Snack = { randomSnack() } }With this, we can assign an instance of
AnyMoneyVendingMachineto a variable whose type isVendingMachine. When assigned to aVendingMachinevariable, it can only accept aCoin. But when assigned to anAnyMoneyVendingMachine, it can accept either aCoinor aBill.val vendingMachine: VendingMachine = AnyMoneyVendingMachine() val anyMoneyMachine: AnyMoneyVendingMachine = AnyMoneyVendingMachine() val snack1: Snack = vendingMachine.purchase(Dime()) val snack2: Snack = anyMoneyMachine.purchase(Dime()) val snack3: Snack = anyMoneyMachine.purchase(OneDollarBill())As we did earlier, we can put these two classes side by side to discover the nature of the variance between the class type and the parameter type of
purchase.This time, we can see that as we make a more specific
VendingMachine, we can accept a more general parameter type inpurchase.The arrows above are pointing in opposite directions, so this kind of variance is called contravariance.
As you recall from the story, when Vinnie tried to replace the vending machine with one that accepted a more specific kind of coin, he broke his contract. Similarly, we can’t create a subclass that accepts a more specific type than its superclass did.
For example, if we update
purchaseso that its parameter type isDime, we’ll get a compiler error.class DimeOnlyVendingMachine : VendingMachine() { override val purchase: (Dime) -> Snack = { randomSnack() } }ErrorSo, we learned from this story that a subtype can declare that it accepts a more general type, but not a more specific type.
Now that we understand variance—including covariance and contravariance—it’s time to revisit what we learned, and see how these concepts apply to generics.
What Makes a Subtype a Subtype?
Whenever Vinnie replaced one vending machine with another, everything was fine as long as the new machine did everything that the contract said it would. Specifically, the contract stated that the vending machine must accept a coin and return a snack. If the new machine did those things, then it was a suitable substitute. On the two occasions when it broke the contract, it was not a suitable substitute.
So what makes a subtype a subtype? The ability to substitute it for one of its supertypes. A subtype must fully comply with the contract of its supertype. Specifically, this means it must obey the following three rules:1
- The subtype must have all of the same public properties and functions as its supertype.
- Its function parameter types must be the same as or more general than the ones in its supertype.
- Its function return types must be the same as or more specific than the ones in its supertype.
We usually think of a subtype as a class that extends another class, an interface that extends another interface, or a class that implements an interface. In all three of these cases, Kotlin’s type system will ensure that the subclass follows the three rules above. However, when it comes to parameterized types—such as
VendingMachine<Snack>andVendingMachine<CandyBar>—we can’t explicitly declare that one type is a subtype of another.To help demonstrate this, let’s convert
VendingMachineto a generic class. As a part of this change, we’ll add asnackconstructor parameter, which is the snack that will get returned when thepurchase()function is called.class VendingMachine<T : Snack>(private val snack: T) { fun purchase(money: Coin): T = snack }As we learned in the last chapter, we can create parameterized types from this generic, such as
VendingMachine<Snack>andVendingMachine<CandyBar>. Even though we can’t explicitly declare thatVendingMachine<CandyBar>is a subtype ofVendingMachine<Snack>, it would be perfectly safe for this to be the case, because it wouldn’t break the contract—all three rules above would be satisfied.We can visualize this by imagining what the effective parameterized type would look like. In other words, let’s take the generic
VendingMachineabove, and everywhere that the type parameter appears, we’ll replace it with the type argument. Are all three rules satisfied?So,
VendingMachine<CandyBar>fully satisfies the contract ofVendingMachine<Snack>, which means it’s safe for it to be one of its subtypes. This won’t just happen automatically, though. For example, if we try assigning it to a variable declared asVendingMachine<Snack>, we’ll get a compiler error.val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar()) val vendingMachine: VendingMachine<Snack> = candyBarMachineErrorSo,
VendingMachine<CandyBar>won’t be a subtype ofVendingMachine<Snack>until we tell Kotlin our intent, which requires us to do one more thing.Variance Modifiers
As we saw above,
VendingMachine<CandyBar>fully satisfies the contract ofVendingMachine<Snack>, so it should be possible for it to be a subtype of it. However, a type parameter can potentially be used in lots of places throughout the body of a generic type. It could be used as the return type of a function, as the parameter of a function, as the type of a property, and so on. Let’s consider what would happen if we were to add arefund()function to theVendingMachineinterface.class VendingMachine<T : Snack>(private val snack: T) { fun purchase(money: Coin): T = snack fun refund(snack: T): Coin = Dime() }In this case, the type parameter
Tappears both as a function result type and as a function parameter type. Would this version ofVendingMachine<CandyBar>satisfy the contract ofVendingMachine<Snack>? Let’s compare the effective parameterized types again. First, let’s look at thepurchase()function.As before, when it comes to the
purchase()function,VendingMachine<CandyBar>satisfies the contract ofVendingMachine<Snack>. So far so good. Now let’s look at therefund()function.Yikes! Even though the return type of
refund()satisfies the contract, the parameter type does not, becauseCandyBaris not the “same as or more general than”Snack—it’s more specific. So, ifVendingMachine<T>includes therefund()function, thenVendingMachine<CandyBar>cannot be a subtype ofVendingMachine<Snack>.If Kotlin is going to treat
VendingMachine<CandyBar>as a subtype ofVendingMachine<Snack>, we have to promise Kotlin that we won’t use this type parameter as a function parameter type, like we did inrefund(). Instead, it can only publicly appear in *out-positions—in other words, as a function return type, or as the type of a read-only property.To make this promise, we can add a variance modifier to the type parameter. Kotlin has two variance modifiers—let’s check them out.
The
outmodifierThe first variance modifier is named
out, and it’s our way of telling Kotlin that this type parameter will only appear in an out-position. Let’s add theoutmodifier to type parameterT.class VendingMachine<out T : Snack>(private val snack: T) { fun purchase(money: Coin): T = snack }This simple change is all that’s needed to get the code from Listing 19.14 to compile without errors—
VendingMachine<CandyBar>is now a subtype ofVendingMachine<Snack>!val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar()) val vendingMachine: VendingMachine<Snack> = candyBarMachineAs we saw earlier in this chapter, a type is covariant with its associated function result types. So by ensuring that this type parameter is only used as a result type, we know that it’s safe for the
VendingMachinetype to be covariant onT.Again, the
outmodifier is a promise to Kotlin that we will only use the type parameter in the out-position. If we try to use this same type parameter publicly in an *in-position—that is, as a function parameter type—then we’ll get a compiler error. To demonstrate this, we can pop in therefund()function from Listing 19.15, and watch as the compiler calls us out for breaking our promise.class VendingMachine<out T : Snack>(private val snack: T) { fun purchase(money: Coin): T = snack fun refund(snack: T): Coin = Dime() }ErrorSo, by adding the
outmodifier to the type parameter, we’ve got the advantage thatVendingMachine<CandyBar>is now a subtype ofVendingMachine<Snack>, but we’ve got the disadvantage that we can no longer useTin an in-position.The
inmodifierAs you probably guessed, Kotlin includes a complement to the
outmodifier, calledin. But before we look at it, let’s shake things up in theVendingMachineclass. Instead of a type parameter for its snack type, let’s use the type parameter for its money type.class VendingMachine<T : Money> { fun purchase(money: T): Snack = randomSnack() }Similar to before, we can try to assign an instance of
VendingMachine<Money>toVendingMachine<Coin>, but we’ll get an error.val moneyVendingMachine: VendingMachine<Money> = VendingMachine() val coinVendingMachine: VendingMachine<Coin> = moneyVendingMachineErrorAlthough
Tis only being used in the in-position, we still have to tell Kotlin by using theinvariance modifier.class VendingMachine<in T : Money> { fun purchase(money: T): Snack = randomSnack() }With this change, the code from Listing 19.20 successfully compiles!
val moneyVendingMachine: VendingMachine<Money> = VendingMachine() val coinVendingMachine: VendingMachine<Coin> = moneyVendingMachineAs with the
outmodifier, we’ve made a trade-off here: when we declare a type parameter with theinmodifier, we’re promising Kotlin that it will only ever appear in the in-position. Again, if we add arefund()function, we would needTin the out-position—as that function’s return type—which would cause a compiler error.class VendingMachine<in T : Money>(private val money: T) { fun purchase(money: T): Snack = randomSnack() fun refund(snack: Snack): T = money }ErrorTo summarize:
- The
outmodifier can be used to ensure that the type parameter will only publicly appear in an out-position, which makes it safe for covariance.- Conversely, the
inmodifier can be used to ensure that it will only publicly appear in an in-position, so that it’s safe for contravariance.In order to keep things simple, we’ve only used one type parameter at a time so far in this chapter. It’s entirely possible to have multiple type parameters, though, and when we do, we can use a variance modifier on each one. Let’s see how that looks.
Variance on Multiple Type Parameters
It’s important to note that generic variance doesn’t describe the type as a whole—it describes the relationship of the type to one of its type parameters. So, it can be covariant on one and contravariant on another. Instead of a single type parameter for either the
MoneyorProduct, let’s include one for each.class VendingMachine<in T : Money, out R: Product>(private val product: R) { fun purchase(money: T): R = product }In this code,
VendingMachineis contravariant onT, and covariant onR. From this generic class, we could instantiate a wide range of parameterized types.Some of them would include:
With two type parameters, all those types of money, and all those types of products, it’s easy to get lost in which of the parameterized types are subtypes of the others. Just remember that a subtype must fully support the contract of its supertype. Here’s how the type hierarchy looks for the parameterized types above.
It’s helpful to know that we can apply variance modifiers to multiple type parameters. For the rest of this chapter, though, we’ll go back to a single type parameter in order to keep the examples easy to follow.
We’ve seen how the
inandoutmodifiers create variance, but we’ve also seen the trade-offs—type parameters declared withincan’t be used as a result type, and those declared withoutcan’t be used as a function parameter type. But sometimes, those trade-offs just won’t cut it. When that’s the case, we can still get some of the benefits of variance by using type projections.Type Projections
So far, we’ve been able to get
VendingMachine<CandyBar>to be a subtype ofVendingMachine<Snack>by using variance modifiers on our type parameters. Let’s update the code from Listing 19.16 so that it works with any kind ofProductand any kind ofMoney.class VendingMachine<out T : Product>(private val product: T) { fun purchase(money: Money): T = product }In this code, we’ve got
outon the type parameter again. Because we put this variance modifier where the type parameter is declared, this is called declaration-site variance. We can’t always use declaration-site variance, though. For example, what if we really, truly need thatrefund()function from Listing 19.18?class VendingMachine<T : Product>(private val product: T) { fun purchase(money: Money): T = product fun refund(product: T): Money = Dime() }We can’t use the
outvariance modifier, becauseTis used in an in-position inrefund(). And we can’t use theinmodifier, because it’s used in an out-position inpurchase(). Are we just out of luck, here? Does this mean thatVendingMachine<CandyBar>will never be considered a subtype ofVendingMachine<Snack>?Thankfully, Kotlin provides a second option. Instead of using a variance modifier on a type parameter, we can use it on a type argument. To demonstrate this, let’s start with a function that accepts a
VendingMachine<Snack>.fun getSnackFrom(machine: VendingMachine<Snack>): Snack { return machine.purchase(Dime()) }Since there are no variance modifiers on the type parameter in Listing 19.26,
VendingMachine<CandyBar>is not a subtype ofVendingMachine<Snack>, so we won’t be able to call the function with an instance ofCandyBarMachine.val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar()) getSnackFrom(candyBarMachine)ErrorNow let’s add an
outmodifier to the type argument in thegetSnackFrom()function, like this.fun getSnackFrom(machine: VendingMachine<out Snack>): Snack { return machine.purchase(Dime()) }With this change, the code from Listing 19.28 compiles successfully!
val candyBarMachine: VendingMachine<CandyBar> = VendingMachine(CandyBar()) getSnackFrom(candyBarMachine)When we put a variance modifier on a type argument rather than a type parameter, we create variance at the place in our code where we’re using the type (e.g.,
getSnackFrom()) instead of where we declared it (e.g.,VendingMachine). For this reason, we call this use-site variance.Just like declaration-site variance, use-site variance comes with trade-offs. As we’ll see in a moment, because the
machineparameter has theoutmodifier, we won’t be able to call therefund()function inside the body of this function. Thankfully, the body of this function has no need for therefund()function, so this trade-off is entirely acceptable here!Keep in mind that declaration-site variance applies to the whole project, but use-site variance works only in the specific part of the project where we put the variance modifier on the type argument. In the example code above, it only works for the
getSnackFrom()function. So, if other functions in our project still need to use therefund()function, that’s completely fine. In those places,VendingMachine<CandyBar>simply wouldn’t be able to be a subtype ofVendingMachine<Snack>.Out-Projections
It’s important to note that, within the body of this function,
machinedoes not have the typeVendingMachine<Snack>. It has the typeVendingMachine<out Snack>, which is a type projection. Since this type argument has theoutmodifier on it, this particular kind of type projection is called an out-projection.What exactly is a projection?
![]()
Think of a projection like a shadow. If you shine a light on a ball, it casts a shadow onto the wall. The ball itself is a sphere that has three dimensions, but the ball’s shadow on the wall is a circle that has only two dimensions. The shadow still resembles the ball, but it’s missing its depth.2
Similarly, when we create a type projection, it’s kind of like we’re removing some of the “depth” of that object by limiting the types of its function inputs and outputs.
For example, in the
getSnackFrom()function above, we created a type projection fromVendingMachine<Snack>, calledVendingMachine<out Snack>, which looks a lot like the original, except that therefund()function no longer accepts aSnack. Instead, it accepts a type calledNothing!What exactly is
Nothing? As mentioned in Chapter 14, every type in Kotlin is a subtype of a class namedAny. Similarly, every type in Kotlin is also a supertype of a class calledNothing.The
Nothingclass can never be instantiated, because it only has aprivateconstructor. Since it’s impossible to create an instance ofNothing, we’ll never be able to call therefund()function.So, an out-projection is created by adding the
outmodifier to a type argument. It looks similar to the original parameterized type, but everywhere that the type argument appears in an in-position, it is replaced with theNothingtype.In-Projections
As you can probably guess, we can also use an
inmodifier to create an in-projection, like in this function.fun getRefundFrom(machine: VendingMachine<in CandyBar>): Money { return machine.refund(CandyBar()) }As with the out-projection above, inside the body of this function, we end up with a projection of
VendingMachine<CandyBar>. But this time, instead of affecting the functions’ parameter types, it’s the functions’ result types that have been affected.With an in-projection, everywhere that the type argument appears in an out-position, it is forced to
Any?.It’s entirely possible to call these functions and get a result. If we want to do anything useful with that result, though, we’ll probably need to cast it back to its more specific type.
So, out-projections and in-projections are two kinds of type projections.
- An out-projection creates covariance by forcing the type argument in in-positions to
Nothing.- An in-projection creates contravariance by forcing the type argument in out-positions to
Any?.Sometimes we want a function to accept all instances of a generic type, regardless of their type arguments. For these cases, Kotlin includes one more kind of projection.
Star Projections
Remember Vinnie? Well, at the end of the month, he services all of his vending machines, performing basic maintenance in order to keep them finely-tuned and working well. This operation doesn’t involve any money, snacks, or toys. He just needs to get in there and tighten a few bolts.
Here’s an updated version of
VendingMachinethat includes a function namedtune(). As you can see, this new function doesn’t use a type parameter at all—neither as a function parameter nor as a return type.class VendingMachine<T : Snack>(private val snack: T) { fun purchase(money: Coin): T = snack fun refund(snack: T): Coin = Dime() fun tune() = println("All tuned up!") }All vending machines need to be tuned up. The type of the
Snackthat they return is completely irrelevant.In cases like these, we might want a function to accept literally any kind of type created from
VendingMachine. Kotlin makes this easy with a special kind of type projection called a star-projection. To create a star-projection, rather than using a variance modifier, simply use an asterisk*(that is, a “star”) in place of the type argument. For example, here’s a function that will accept any kind ofVendingMachine, regardless of its type argument.fun service(machine: VendingMachine<*>) { print("Tuning up $machine... ") machine.tune() }This function can be called with any kind of
VendingMachine, regardless of its type argument.service(VendingMachine(CandyBar())) // Works with VendingMachine<CandyBar> service(VendingMachine(TrailMix())) // Works with VendingMachine<TrailMix> service(VendingMachine(GummyBears())) // Works with VendingMachine<GummyBears>A star-projection looks similar to the original parameterized type, but it changes it in these ways.
- Anywhere that the type parameter was used in an in-position, it’ll be replaced with the
Nothingtype.- Anywhere that the type parameter was used in an out-position, it’ll be replaced with the type parameter’s upper bound. Remember—if no upper bound was specified, it’ll be
Any?by default.In summary, star-projections are useful when we want to accept any instance of a generic type, regardless of its type arguments.
Variance in the Standard Library
Now that we’ve learned all about covariance, contravariance, variance modifiers, and type projections, we can better understand why certain types in the standard library work the way that they do.
We opened this chapter by presenting the fact that
MutableList<Cow>is a subtype ofList<Cow>, but it’s not a subtype ofMutableList<FarmAnimal>. Why is this?
MutableList<Cow>is a subtype ofList<Cow>becauseMutableListextends theListinterface. That’s just regular interface inheritance.
MutableList<Cow>is not a subtype ofMutableList<FarmAnimal>because it allows you to both read and modify its elements, which means its type parameter appears in both the in-position and out-position. As a result, it can’t have a variance modifier on it. This is similar to ourVendingMachinein Listing 19.26.As we’ve seen, the collection types in Kotlin usually come in two flavors—a read-only type (e.g.,
List) and a mutable type (e.g.,MutableList).
- The read-only types will have the
outmodifier on their type parameters, soList<Cow>will be a subtype ofList<FarmAnimal>.However, the mutable types _won’t_ have any variance modifiers, soMutableListwill _not_ be a subtype ofMutableList`. As you now know, though, you can use a type projection to work around this when it makes sense!
Summary
Now that Parker and Vinnie understand which vending machines honor the contract, they can confidently replace them without making any parents angry or kids hungry. And now that you understand how generics form subtypes, you can confidently use variance without breaking your code!
Congratulations on working your way through this chapter! Here’s a recap of what we covered:
- How covariance describes the relationship between a type and its function return types.
- How contravariance describes the relationship between a type and its function parameter types.
- How we can use variance modifiers on type parameters to create declaration-site variance.
- How we can use variance modifiers on type arguments to create use-site variance.
- How collection types in the standard library use variance modifiers.
You’ve come a long way since the first chapter—starting with the basics of variables and expressions, building up to generics and variance. Now, for our final chapter, we’ll explore one of Kotlin’s most exciting topics—~coroutines!_
The goal of this chapter is to explain generic variance, so these three rules are focused on the structure of the types. More strictly speaking, though, the behaviors of the classes should be compatible, as well. (See Liskov, B., & Wing, J. M. (1994). A behavioral notion of subtyping. ACM Transactions on Programming Languages and Systems, 16(6), 1811-1841). Also, Meilir Page-Jones observes these three rules more generally through the lens of preconditions, postconditions, and class invariants. (Page-Jones, M. (2000). Fundamentals of Object-Oriented Design in UML. Addison-Wesley Professional. p. 283). ↩︎
The word “projection” in the term “type projection” is technically rooted in mathematics, but the idea is the same. ↩︎