...

/

Generics: Variance and Constraints of Parametric Types

Generics: Variance and Constraints of Parametric Types

The desire to reuse code shouldn’t come at a cost of compromising type safety. Generics bring a nice balance to this issue. With generics you can create code that can be reused for different types. At the same time, the compiler will verify that a generic class or a function isn’t used with unintended types. We’ve enjoyed a fairly good amount of type safety with generics in Java. So, you may wonder, what could Kotlin possibly provide to improve? It turns out, quite a bit.

By default, in Java, generics impose type invariance—that is, if a generic function expects a parametric type T, you’re not allowed to substitute a base type of T or a derived type of T; the type has to be exactly the expected type. That’s a good thing, as we’ll discuss further in this section. But what good are rules if there are no exceptions?—and it’s in the area of exceptions that Kotlin comes out ahead.

We’ll first look at type invariance and how Kotlin, just like Java, nicely supports that. Then we’ll dig into ways to change the default behavior.

Sometimes you want the compiler to permit covariance—that is, tell the compiler to permit the use of a derived class of a parametric type T—in addition to allowing the type T. In Java you use the syntax <? extends T> to convey covariance, but there’s a catch. You can use that syntax when you use a generic class, which is called use-site variance, but not when you define a class, which is called declaration-site variance. In Kotlin you can do both, as we’ll see soon.

Other times you want to tell the compiler to allow contravariance—that is, permit a superclass of a parametric type T where type T is expected. Once again, Java permits contravariance, with the syntax <? super T> but only at use-site and not declaration-site. Kotlin permits contravariance both at declaration-site and use-site.

In this section we’ll first review type variance, which is available in Java. Going over it here will help set the context for the much deeper discussions to follow. Then, you’ll learn the syntax for covariance both for declaration-site and use-site. After that, we’ll dive into contravariance and, finally, wrap up with how to mix multiple constraints for variance.

Type invariance

When a method receives an object of a class T, you may pass an object of any derived class of T. For example, if you may pass an instance of Animal, then you may also pass an instance of Dog, which is a subclass of Animal. However, if a method receives a generic object of type T, for example, List<T>, then you may not pass an object of a generic object of derived type of T. For example, if you may pass List<Animal>, you can’t pass List<Dog> where Dog extends Animal. That’s type invariance—you can’t vary on the type.

Let’s use an example to illustrate type invariance first, and then we’ll build on that example to learn about type variance. Suppose we have a Fruit class and two classes that inherit from it:

// typeinvariance.kts
open class Fruit
class Banana : Fruit()
class Orange: Fruit()

Now suppose a basket of Fruits is represented by Array<Fruit> ...