Wildcards in Java generics (Part 1/3)
Covariance: Why List<Cat> is not assignable to List<Animal>?
I often find myself trying to assign a lists of cats (List<Cat>) to a list of animals (List<Animal>). This is not possible in java and it led me down a rabbit hole of reading about something called in/co/contra-variance. I would like to share the (very, very) simplified findings, one “variance” at a time. Starting with invariance and covariance.
1) In java, List<Cat> is not a subtype of List<Animal>. This is annoying but safe.
2) In java, both List<Cat> and List<Animal> are subtypes of List<? extends Animal>. This is less annoying but dangerous.
3) When using <? extends BaseType>, it is generally (type-) safer if it describes a return type (example List::get). It is generally more (type-) dangerous, if <? extends BaseType> describes an input parameter type (example List::add).
4) This picture is going to make sense after Part 3/3:
Let’s dive into the details.
Consider the age old example of base/derived classes Animal, Cat and Dog.
Cat is a subclass of Animal, and so is Dog. All animals have a name and are able to speak. The name is a string kept in the base class Animal, and String speak() is an abstract method that is overriden in both Cat and Dog:
It would be good to brush up on the definition of subclasses too. If an object of class Animal can be seamlessly changed with an object of class Cat, it means (at least in theory) that Cat is a subclass of Animal. If an object of class C is assignable to an object of class A, C is a subclass of A. Let’s get to the problem at hand:
At first glance, it seems fairly reasonable to assume that an object reference of List<Cat> can be assigned to a reference of List<Animal>. This is not the case because List<Cat> and List<Animal> are invariant. Invariant just means that they are two completely unrelated types. Weird? Sure, but here is an example of why this is so:
In [line 2], we create a cat list. We assign that catList to an animalList (so far so good?). But then we add a dog to the animalList reference, which in turn points to the catList. This is obviously a problem, because now we have a dog in a catList. Hence, this example would not compile.
Assigning a cat or a dog to an animal reference, is just fine though and is the reason why the assignment of [line 4] above must work. (This is also the basis of polymorphism, so it is kinda more important than assigning cat lists to dog lists):
Now would be a good time to define “variance”. It really doesn’t exist like plain “variance”, it can be either covariance, contravariance or invariance.
In/Co/Contra-variance is used to define the relationship of complex types (usually generic) in terms of the relationship of the simpler types they are made of (usually the generic type argument).
Yeah, I didn’t understand that either. Let’s try some examples.
Invariance means List<Animal> has nothing to do with List<Cat> (even though it doesn’t feel that way). List<Animal> has the same relationship with List<Cat> as it has with List<Double>; it couldn’t care less. As we can see in the second example [line 4], all of this is because of that pesky mutability of List.
If List<Cat> was assignable to List<Animal>, it would have been covariant. Luckily, a covariant example exists in Java, albeit with its own drawbacks, that seem surprisingly similar to the List example. Arrays are covariant in java:
This means that we can assign both a catArray to the animalsArray, and a dogsArray to the animalsArray.
Simply put, if GenericType<Cat> can be assigned to GenericType<Animal>, implying that GenericType is covariant over the single generic argument.
This is the case with arrays in java (even though it doesn’t really use the <> syntax). If a GenericType<Cat> cannot be assigned to GenericType<Animal> or vice versa, it means that they are invariant over the generic argument just like the List<> in java.
One throwback to the covariance of animal arrays: Even though arrays are covariant in java, a perfectly compilable code can result in runtime errors, like in [line 5] below.
So, be careful with your cats and dogs.
At this point, we should have a clear concept of Invariance and Covariance.
To make things sound more maths-y:
Saying that GenericType<T> is covariant over T means: If Cat is a subtype of Animal, then GenericType<Cat> is a subtype of GenericType<Animal>. This is the case with arrays.
Saying that GenericType<T> is invariant over T means: The relationship between Animals and Cats doesn’t matter; GenericType<Animal> has (almost) nothing to do with Generic<Cat>.
In java, we can battle invariance if we use the wildcard generic argument. We can make generic types covariant by using the generic wildcard <? extends T>. We can say that we need to assign both a GenericType<Cat> or a GenericType<Dog> to a common reference by using (GenericType<? extends Animal>). This is how that looks in java code:
Bear in mind, that List<? extends Animal> is a new type different from List<Animal>, and it is a super class of all: List<Animal>, List<Cat> and List<Dog>. Modifying the base type (List<? extends Animal>) can be tricky. Adding anything to List<? extends Animal>, that is not of the actual generic type that the base reference is pointing to, will result in a runtime error, even though it compiles just fine: see [line 4] below.
Lists in java are invariant. This is anoying. We can make lists in java covariant. This is dangerous. In order to live dangerously (but intuitively), we should use the wildcard (<? extends BaseClass>).
As a general rule of thumb, we should use covariance (<? extends BaseClass>) in a collection, if we only want to read values out of it, or do generic type agnostic actions (reordering, removing elements, etc.), but not if we want to add stuff to it.
Generally with the wildcard (<? extends BaseClass>), it is safer for use if it describes the return type of a function (think List::get).
The wildcard (<? extends BaseClass>) can be dangerous in non-obvious ways, if it describes the input parameter type (think List:: add).
Use covariance for return types and prosper.