Writing flexible library-style APIs — and decoding confusing compiler errors — needs a real model of how generics work.
Generic types are invariant: even though Integer is a Number, a List<Integer> is not a List<Number>. If it were, you could add a Double to a list of integers through the List<Number> view. Wildcards restore flexibility safely:
List<? extends Number>— a list of some subtype of Number. You can read elements asNumber, but can't add (exceptnull), because the exact type is unknown.List<? super Integer>— a list of Integer or a supertype. You can addIntegers, but reading only gives youObject.List<?>— any list; read asObject.
The rule of thumb is PECS: Producer Extends, Consumer Super. A parameter you read from is a producer (extends); one you write into is a consumer (super). That's why Collections.copy is declared copy(List<? super T> dest, List<? extends T> src), and why comparator parameters are usually Comparator<? super T>.
Generics are implemented by type erasure: the compiler checks types, then erases type parameters to their bounds (usually Object) and inserts casts. At runtime a List<String> and a List<Integer> are the same class. Consequences:
- No
new T(), nonew T[n], noinstanceof List<String>— pass aClass<T>or aSupplier<T>when you need to create instances. - You can't overload
m(List<String>)andm(List<Integer>); they erase to the same signature. - Mixing generics with arrays or raw types leads to heap pollution and "unchecked" warnings. Treat those warnings as bugs unless you can prove safety, then suppress narrowly with
@SuppressWarnings("unchecked").
Use wildcards in method parameters, not in return types — callers shouldn't have to deal with them.
Example
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Supplier;
public class Wildcards {
static double sum(List<? extends Number> nums) { // producer: extends
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
static void addOnes(List<? super Integer> sink, int count) { // consumer: super
for (int i = 0; i < count; i++) sink.add(1);
}
static <T> T make(Supplier<T> factory) { return factory.get(); } // instead of new T()
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2.5, 3L))); // 6.5
List<Number> numbers = new ArrayList<>();
addOnes(numbers, 2); // List<Number> accepts Integers
System.out.println(numbers); // [1, 1]
List<String> words = new ArrayList<>(List.of("b", "a"));
words.sort(Comparator.comparing(Object::toString)); // Comparator<? super String>
System.out.println(make(StringBuilder::new).append(words)); // [a, b]
}
}Remember
- Generic types are invariant:
List<Integer>is not aList<Number> - PECS — read from
? extends T, write into? super T - Type parameters are erased at runtime: no
new T(), noinstanceof List<String>
Watch out for
- Returning wildcard types from public methods, forcing every caller to handle them.