List<Integer> is not a List<Number>. Wildcards (?, ? extends, ? super) restore flexibility, PECS tells you which to use, and type erasure explains generics' odd limitations.
Generics are invariant
Even though Integer is a Number, List<Integer> is not a subtype of List<Number>. If it were, you could add a Double to a list of integers through the List<Number> reference.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // not allowed
nums.add(3.14);
}
}Main.java:6: error: incompatible types: List<Integer> cannot be converted to List<Number>
List<Number> nums = ints; // not allowed
^Arrays are covariant (Integer[] is an Object[]), so the same mistake compiles and only fails at runtime:
public class Main {
public static void main(String[] args) {
Object[] objs = new Integer[2]; // compiles: arrays are covariant
objs[0] = 1;
try {
objs[1] = "text"; // runtime type check
} catch (ArrayStoreException e) {
System.out.println("ArrayStoreException: " + e.getMessage());
}
}
}ArrayStoreException: java.lang.StringThe three wildcards
| Wildcard | Accepts | Read as | Can add? |
|---|---|---|---|
List<?> | A list of any type | Object | Only null |
List<? extends Number> | List<Number>, List<Integer>, List<Double>… | Number | No (except null) |
List<? super Integer> | List<Integer>, List<Number>, List<Object> | Object | Yes, Integers |
import java.util.*;
public class Main {
static double sum(List<? extends Number> nums) { // producer: we only read
double s = 0;
for (Number n : nums) s += n.doubleValue();
return s;
}
static void fill(List<? super Integer> sink, int n) { // consumer: we only write
for (int i = 1; i <= n; i++) sink.add(i);
}
static int count(Collection<?> anything) { return anything.size(); }
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3))); // List<Integer>
System.out.println(sum(List.of(1.5, 2.5))); // List<Double>
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
fill(numbers, 3);
fill(objects, 2);
System.out.println(numbers + " " + objects);
System.out.println(count(Set.of("a", "b")));
}
}6.0
4.0
[1, 2, 3] [1, 2]
2PECS: Producer Extends, Consumer Super
If a parameter produces Ts for you → ? extends T. If it consumes Ts → ? super T. Both → plain T.
import java.util.*;
public class Main {
// src produces T (extends), dest consumes T (super) — same shape as Collections.copy
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) dest.add(item);
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2);
List<Double> doubles = List.of(3.5);
List<Number> all = new ArrayList<>();
copy(all, ints); // T = Integer, Number is a super of it
copy(all, doubles); // T = Double
System.out.println(all);
List<Object> objs = new ArrayList<>(List.of("x"));
Main.<Integer>copy(objs, ints);
System.out.println(objs);
}
}[1, 2, 3.5]
[x, 1, 2]JDK examples: Collections.copy(List<? super T> dest, List<? extends T> src), addAll(Collection<? extends E>), sort(Comparator<? super E>) — a Comparator<Object> can sort a list of strings.
Type erasure
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());
Object o = a;
System.out.println(o instanceof List<?>); // OK: unbounded wildcard
// o instanceof List<String> // error: can't check a type argument at runtime
}
}true
java.util.ArrayList
true| You can't… | Because at runtime T is erased | Workaround |
|---|---|---|
new T() | The JVM doesn't know what T is | Pass a Supplier<T> or Class<T> |
new T[n], new List<String>[n] | Arrays need their exact element type at runtime | Use a List<T>, or (T[]) new Object[n] with care |
x instanceof List<String> | Type arguments aren't stored | instanceof List<?> |
Overload f(List<String>) and f(List<Integer>) | Both erase to f(List) — same signature | Use different method names |
catch (T e) / generic exception classes | Catch needs a runtime type | — |
| Use primitives as type arguments | Erasure turns T into Object | Boxed types, or specialised streams (IntStream) |
import java.util.*;
import java.util.function.Supplier;
public class Main {
static <T> List<T> nCopies(int n, Supplier<T> factory) { // instead of new T()
List<T> out = new ArrayList<>();
for (int i = 0; i < n; i++) out.add(factory.get());
return out;
}
static <T> T create(Class<T> type) throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance();
}
public static void main(String[] args) throws Exception {
System.out.println(nCopies(3, StringBuilder::new).size());
System.out.println(create(ArrayList.class).isEmpty());
}
}3
trueBridge methods and heap pollution
When a class implements Comparable<Money>, its compareTo(Money) erases differently from the interface's compareTo(Object). The compiler generates a synthetic bridge method compareTo(Object) that casts and delegates, so polymorphism still works after erasure.
import java.lang.reflect.Method;
public class Main {
record Money(long cents) implements Comparable<Money> {
public int compareTo(Money o) { return Long.compare(cents, o.cents); }
}
public static void main(String[] args) {
java.util.List<String> found = new java.util.ArrayList<>();
for (Method m : Money.class.getDeclaredMethods())
if (m.getName().equals("compareTo"))
found.add("compareTo(" + m.getParameterTypes()[0].getSimpleName() + ")"
+ (m.isBridge() ? " <- bridge" : ""));
java.util.Collections.sort(found);
found.forEach(System.out::println);
}
}compareTo(Money)
compareTo(Object) <- bridgeHeap pollution happens when a variable of a parameterised type refers to an object that isn't of that type — typically through raw types, unchecked casts or generic varargs (T... creates a T[]). The compiler warns (unchecked); @SafeVarargs marks a varargs method that doesn't misuse its array.
What is PECS?
Why is List<Integer> not a subtype of List<Number>?
What is type erasure and what are its consequences?
List<?> vs List<Object>?