Variance: why arrays and generics disagree

OOP · lesson 43 of 43 · 6 min read

Arrays are covariant and unsafe; generics are invariant and need wildcards.

Open this lesson in the learning hub

Key points

  • Arrays are covariant: String[] is a Object[]. That is unsound, so the JVM checks every store at runtime and throws ArrayStoreException when it is wrong.
  • Generics are invariant: List<String> is not a List<Object>. That restriction is what moves the error to compile time instead of runtime.
  • Wildcards restore the flexibility safely. ? extends T is a producer you may read from, and ? super T is a consumer you may write to.
  • That is PECS - Producer Extends, Consumer Super. It is not a style rule; it is what the type system can actually prove.
  • From a List<? extends Number> you may read a Number but add nothing except null, because the actual element type is unknown - it could be a List of Integer.
  • Because of erasure, List<String> and List<Integer> are the same class at runtime, which is why you cannot overload on them and cannot create a generic array.

Example

import java.util.ArrayList;
import java.util.List;

public class VarianceAndPecs {

    // PRODUCER - we only READ, so ? extends.
    static double sum(List<? extends Number> producer) {
        double total = 0;
        for (Number n : producer) { total += n.doubleValue(); }
        // producer.add(1);   // would not compile - element type is unknown
        return total;
    }

    // CONSUMER - we only WRITE, so ? super.
    static void fill(List<? super Integer> consumer, int count) {
        for (int i = 0; i < count; i++) { consumer.add(i); }
        // Integer x = consumer.get(0);   // would not compile - only Object is known
    }

    public static void main(String[] args) {
        // ARRAYS: covariant, checked at RUNTIME.
        String[] strings = {"a", "b"};
        Object[] objects = strings;              // legal - and unsound
        System.out.println("String[] assigned to Object[]: allowed at compile time");
        try {
            objects[0] = 42;
        } catch (ArrayStoreException e) {
            System.out.println("  storing an Integer -> ArrayStoreException at RUNTIME");
        }

        // GENERICS: invariant, caught at COMPILE time.
        List<String> stringList = new ArrayList<>();
        // List<Object> objectList = stringList;   // does not compile - and that
        //                                            is the whole point
        System.out.println();
        System.out.println("List<String> to List<Object>: rejected at COMPILE time");

        // PECS in use.
        List<Integer> ints = List.of(1, 2, 3);
        List<Double> doubles = List.of(1.5, 2.5);
        System.out.println();
        System.out.println("sum(List<Integer>) = " + sum(ints));
        System.out.println("sum(List<Double>)  = " + sum(doubles));

        List<Number> numbers = new ArrayList<>();
        fill(numbers, 3);
        System.out.println("fill(List<Number>) = " + numbers);
        List<Object> anything = new ArrayList<>();
        fill(anything, 2);
        System.out.println("fill(List<Object>) = " + anything);

        // ERASURE: both are the same class at runtime.
        System.out.println();
        System.out.println("List<String>.getClass()  = " + stringList.getClass().getName());
        System.out.println("List<Integer>.getClass() = " + numbers.getClass().getName());
        System.out.println("  -> same class, which is why you cannot overload on them");
    }
}

Arrays trade compile-time safety for runtime checks; generics do the opposite, and PECS is what makes them flexible again.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.