Variance: why arrays and generics disagree
Arrays are covariant and unsafe; generics are invariant and need wildcards.
Open this lesson in the learning hubKey points
- Arrays are covariant:
String[]is aObject[]. That is unsound, so the JVM checks every store at runtime and throwsArrayStoreExceptionwhen it is wrong. - Generics are invariant:
List<String>is not aList<Object>. That restriction is what moves the error to compile time instead of runtime. - Wildcards restore the flexibility safely.
? extends Tis a producer you may read from, and? super Tis 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>andList<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.