Covariance and Contravariance (Java)5 min read

Many programming languages support subtyping, for example, MechanicalKeyboard can be a subtype of Keyboard, and according to the Liskov substitution principle, the MechanicalKeyboard instance should be substitutable everywhere the Keyboard instance is needed and still behaves correctly.

Covariance and contravariance can sound scary, but it’s all about the ability to substitute the current type with a less derived or more derived type within a collection of elements. Let PineTree be a subtype of Tree, when we have a List<Tree> and we can use its subtype List<PineTree>, then we say it’s covariance. In contrast, let Apple be the subtype of Fruit, when a list of a subtype is required (List<Apple>) but a list of a more general type can be used (List<Fruit>), we say it’s contravariance. In Java, a collection type can either refer to an array, such as Integer[], or a generic collection such as Collection<T>. Invariance is neither covariance nor contravariance, the collection with type E then the only type E can be used, for example, when a List<Cryptocurrency> is required, you cannot assign a List<Currency> or a List<Bitcoin> to it.

CovarianceContravarianceInvariance
The ability to substitute a collection of a subtype when a more general type is needed.The ability to substitute a collection of a more general type when a collection of a more specific type is needed.When a collection of a specific type is required, only a collection with this type can be used.

Different languages may support variance in different ways, some languages might support covariance and contravariance, some languages might support only covariance, and some languages might support none. Let’s have some concrete examples in Java.

Arrays in Java are covariant:

// assign to a variable
Number[] numbers = new Integer[50]; 

// passing a more derived type when a more general type is required 
public int countPrimes(Number[] numbers) {
    int count = 0;
    for (int i = 0; i < numbers.length; ++i) {
        if (isPrime(numbers[i])) count++;
    }
    return count;
}
countPrimes(new Integer[] {1, 2, 3, 4, 5, 6, 7});

// return a more specific type when a more general type is required
public Number[] getNums() {
    return new Integer[] {1, 2, 3, 4, 5};
}

Because of this property, sometimes we can run into a situation where the code is compiled fine, but later on, we get the ArrayStorageException at runtime:

Object[] objects = new Integer[] {1, 2, 3};
objects[0] = new String("A hideous string"); // fail at runtime

However, generic collections in Java are invariant:

// assigning to a variable
List<Number> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> integers = List.of(6, 7, 8, 9, 10);
List<Integer> foo = integers; // ok
foo = numbers; // compile error 
List<Object> objects = new ArrayList<String>(); // compile error

// passing as an argument
public List<PageInfo> getPageInfos(List<Page> pages) {...}
List<Page> pages = ...;
getPageInfos(pages); // ok
List<SubPage> subPages = ...;
getPageInfos(subPages); // not compile, incompatible type!

// returning as a value in a method
List<Accomodation> getAllAccommodations() {
    return new ArrayList<Accommodation>(); // ok
}
List<FiveStarReview> getFiveStarReviews() {
    return new ArrayList<Review>(); // compile error!
}

Generics in Java are type-safe, you cannot substitute a collection of type T when type X is required (even if X is the parent or child of T or whatever), so most of the time you may want to use generic types instead of arrays because you better get some errors at compile time rather than at runtime.

But what about a generic array? Is this possible to create a generic array in Java? The answer is no because if it’s legal, then type safety will not be preserved in the first place. Consider this example, if generic arrays were legal, we would get the ClassCastException at runtime:

List<Integer>[] intLists = new List<>[1]; (1)
Object[] objects = intLists; (2)
List<String> stringLists = List.of("learntocodetogether"); (3)
objects[0] = stringLists; (4)
Integer intVal = intLists[0].get(0); (5) // ClassCastException

If (1) was legal, then (2) should be fine because arrays are covariant, then in (4) because objects[0] is an object with the Object type, then it would also be fine to assign it to the list of strings. And finally, we get an exception from the generic type at runtime because of an “incompatible casting type”. So that is why (1) would result in compile error in the first place.

Let’s have some classes to demonstrate why we should favor generics over arrays:

public class RandomObjects {
     Object[] objects;
     
     public RandomObjects(Collection collection) {
         objects = collection.toArray();
     }
     
     public Object getOne() {
          ThreadLocalRandom rand = ThreadLocalRandom.current();
          return objects[rand.nextInt() % objects.length];
     }
}

There are a lot of problems with this class, first, off we have an array of objects, and we are able to add elements to it with any type. Secondly, in the constructor, we are passing a collection without providing the type parameter, which in turn resolves to a collection of Object. And the getOne method it potentially returns anything, so we need to cast it every time to get the desired type if needed, this is error-prone and there is a high chance that the code will fail at runtime.

However, if we properly use generics instead, it will eliminate the problem:

public class RandomObjectsWithGeneric<T> {
      private final List<T> list;
      
      public RandomObjectsWithGeneric(Collection<T> collection) {
            this.list = new ArrayList<>(collection);
      }
      
      public T getOne() {
            ThreadLocalRandom rand = ThreadLocalRandom.current();
            return list.get(rand.nextInt() % list.size());
      }
}

With this class, the getOne method will always return objects with type T, and we never have to cast or get some errors at runtime.

There are some cases that may confuse us, for example:

List<Number> numbers = List.of(1, 2, 3); // ok

The code generates no warning or error because the RHS resolves to the List<Number> when the compiler sees it, not a List<Integer>.

And do not confuse variance with type substitution of collection elements, for example:

Account account = new Account(...);
List<Account> accounts = new ArrayList<>();
accounts.add(account);
...
PremiumAccount premiumAccount = new PremiumAccount(...);
Account firstAccount = accounts.get(0);
firstAccount = premiumAccount; // ok 

In this case, we have the PremiumAccount is the subtype of the Account, so we’re free to substitute an account with a premium account, remember variance only concerns complex types such as an array or a collection of objects.

Previous Article

Buy Me a Coffee