Skip to content
Part IA Michaelmas Term

Covariance and Invariance

Array covariance vs generic invariance

Arrays Are Covariant

In Java, if Dog is a subtype of Animal, then Dog[] is treated as a subtype of Animal[]:

Dog[] dogs = { new Dog(), new Dog() };
Animal[] animals = dogs;  // compiles — Dog[] is an Animal[]

This is called covariance: the array type varies in the same direction as its element type.

The Array Covariance Trap

Covariance is convenient but unsound:

Animal[] animals = new Dog[3];
animals[0] = new Cat();  // compiles (Cat is an Animal) but throws ArrayStoreException at RUNTIME

The compiler allows this because Cat is an Animal — the static type of animals[0] is Animal, and assigning a Cat to an Animal variable is legal. But at runtime, the JVM knows that animals actually references a Dog[], and storing a Cat into a Dog[] is illegal. The JVM throws ArrayStoreException.

This works because Java arrays are reified — they remember their component type at runtime. Every array store includes a runtime check of the component type. The check catches the error, but only at runtime, not at compile time.

Generics Are Invariant

Generics take the opposite approach: List<Dog> is not a subtype of List<Animal>, even though Dog is a subtype of Animal:

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;  // COMPILE ERROR — incompatible types

This is called invariance. The generic type does not vary with its type parameter at all.

Why Invariance Is Safer

If generics were covariant, we would have the same unsoundness that arrays suffer from:

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;     // hypothetical — if generics were covariant
animals.add(new Cat());          // adding Cat to what is actually List<Dog>
Dog d = dogs.get(0);             // ClassCastException — the element is actually Cat

By making generics invariant, the compiler catches the error at the first line — List<Animal> animals = dogs simply does not compile. There is no way to sneak a Cat into a List<Dog> through an Animal-typed variable.

Why Two Different Design Choices?

Arrays and generics were designed at different times with different goals:

AspectArraysGenerics
IntroducedJava 1.0 (1995)Java 5 (2004)
Design priorityFlexibilityType safety
Runtime representationReified — component type known at runtimeErased — type parameter absent at runtime
VarianceCovariant (with runtime checks)Invariant (with wildcards for controlled variance)
Type errors caughtAt runtime (ArrayStoreException)At compile time

The Java designers have acknowledged that array covariance was a mistake that they could not fix without breaking backward compatibility. Generics were the opportunity to do it right.

The Trade-Off

Array covariance lets you write flexible code that operates on arrays of any subtype:

void printAnimals(Animal[] animals) {
    for (Animal a : animals) {
        System.out.println(a.sound());
    }
}

printAnimals(new Dog[3]);  // works — covariance

But the flexibility comes at the cost of deferred type checking — the ArrayStoreException lurking at runtime.

Generic invariance forces you to be explicit about variance through wildcards, which restore flexibility safely:

void printAnimals(List<? extends Animal> animals) {
    for (Animal a : animals) {
        System.out.println(a.sound());
    }
}

printAnimals(new ArrayList<Dog>());  // works — wildcard accepts subtypes

The wildcard approach is more verbose but gives the compiler enough information to prevent improper stores — animals.add(new Cat()) would be rejected because the compiler knows the list could be List<Dog> or List<Cat> and adding a Cat would be unsafe.

Practical Rule

  • Use arrays when the element type is known and fixed, and you accept the runtime-check trade-off.
  • Use generic collections by default for type safety.
  • When you need to accept a range of generic types in a method, use wildcards (? extends T for reading, ? super T for writing) rather than raw types or unchecked casts.