JAVA / GENERICS AND TYPE ERASURE
Wildcards for flexible producer and consumer types
Use ? extends and ? super so methods accept whole families of generic types, and know exactly which reads and writes each wildcard still permits.
What you will learn
- Declare List<? extends Number> for a parameter you only read elements out of
- Declare List<? super Integer> for a parameter you only add elements into
- Explain why add(x) is rejected on List<? extends Number> but add(null) compiles
- Use List<?> when the method ignores the element type, as size() and clear() do
Understanding Wildcards for flexible producer and consumer types
Generic types in Java are invariant: a List<String> is not a List<Object>, even though every String is an Object. The compiler must refuse that assignment, because through a List<Object> reference you could add an Integer to a list that other code still reads as strings. A wildcard is how you recover flexibility at the point of use: List<? extends Number> means "a List of one single unknown type that is Number or below", so List<Integer> and List<Double> both fit the parameter. The ? stands for one specific type that the compiler cannot name, not for "any type at once".
Which wildcard you want follows from the direction the data moves. With ? extends Number the compiler knows every element is at least a Number, so reads are typed as Number; it does not know whether the argument is really a List<Integer> or a List<Double>, so it rejects every add except add(null). With ? super Integer the situation is mirrored: whatever the real element type is, it is a supertype of Integer, so add(anInteger) is always sound, while get can only promise Object. That asymmetry is the entire content of the producer-extends, consumer-super rule.
The mental model worth carrying: a wildcard in a signature is a claim about what the method will do with the argument. List<?> says the element type is irrelevant, which is exactly why size(), isEmpty() and clear() still work on it while add(x) does not. When one parameter is both read and written at the same type, no wildcard fits and you must name the type, as in List<T> or List<Number>. Because a wildcard in a return type forces every caller to cope with the unknown type as well, keep wildcards on the way in.
import java.util.ArrayList;
import java.util.List;
public class Wildcards {
// the list only produces values, so ? extends is enough
static double sum(List<? extends Number> source) {
double total = 0;
for (Number n : source) {
total += n.doubleValue();
}
return total;
}
// the list only consumes values, so ? super is what is needed
static void addSquares(List<? super Integer> sink, int count) {
for (int i = 1; i <= count; i++) {
sink.add(i * i);
}
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> halves = List.of(0.5, 1.5);
System.out.println(sum(ints));
System.out.println(sum(halves));
List<Number> numbers = new ArrayList<>();
addSquares(numbers, 4);
System.out.println(numbers);
List<Object> objects = new ArrayList<>();
addSquares(objects, 2);
System.out.println(objects);
// sum(List.of("a")); // rejected: String is not a Number
// inside sum(), source.add(1) would be rejected too
}
}A wildcard widens a parameter type by leaving its type argument unknown, and the direction of data flow (extends to read, super to write) is what decides which operations stay type-safe.
Worked examples
The unbounded wildcard
Shows what List<?> allows: any argument, reads typed as Object, and no writes other than null.
import java.util.ArrayList;
import java.util.List;
public class AnyList {
static String describe(List<?> list) {
Object first = list.isEmpty() ? null : list.get(0);
return list.size() + " element(s), first = " + first;
}
static void wipe(List<?> list) {
list.clear();
list.add(null);
}
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("ada", "grace"));
List<Integer> ids = List.of(7);
System.out.println(describe(names));
System.out.println(describe(ids));
wipe(names);
System.out.println(describe(names));
}
}Example explained
Line 1List<?> accepts List<String> and List<Integer> alike, while a List<Object> parameter would reject both.
Line 2Inside describe, list.get(0) is typed as Object because the compiler knows only that some type fills the slot.
Line 3list.clear() compiles because its signature never mentions the element type.
Line 4list.add(null) is the one write a List<?> permits, since null belongs to every reference type.
Both wildcards in one signature
A copy method where the source is the producer and the destination is the consumer.
import java.util.ArrayList;
import java.util.List;
public class CopyDemo {
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> src = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(numbers, src);
System.out.println(numbers);
List<Object> mixed = new ArrayList<>(List.of("start"));
copy(mixed, src);
System.out.println(mixed);
// copy(src, numbers); // no T can be both at least Number and at most Integer
}
}Example explained
Line 1? super T on dest means the destination's element type is T or wider, so dest.add(item) can never violate it.
Line 2? extends T on src means every element is at least a T, which is what lets the for-each variable be typed T.
Line 3copy(mixed, src) works with T = Integer even though mixed is a List<Object> already holding a String.
Line 4The commented call fails because the compiler would need a single T satisfying Number <= T <= Integer.
Important notes
Wildcards live at use sites only: List<? extends Number> l = new ArrayList<Integer>(); is fine, but new ArrayList<? extends Number>() and class Box<? extends Number> do not compile.
List<? extends Number> is not an immutable list. clear() and remove(0) still work; only the operations whose signature names the element type become unsafe.
Common mistakes
Calling add on a List<? extends Number> parameter: javac reports a mismatch against a capture type such as CAP#1, and no cast repairs it because the parameter should have been List<? super Integer>.
Reaching for List<Object> as the "accepts anything" parameter type: a List<String> argument will not compile, since generics are invariant; List<?> is the type that accepts any list.
Assuming the two ? in void swap(List<?> a, List<?> b) denote the same type and trying to move an element between them; each is a separate unknown, so only <T> void swap(List<T> a, List<T> b) permits the transfer.
Try it yourself
Change, predict, then run
Write static void moveInts(List<? extends Integer> from, List<? super Integer> to) that copies every element across, and call it with a List<Integer> source and a List<Number> destination. Then swap the two wildcards and read the compile error you get.
Open the Java workspaceCheck your understanding
A method must read every element of a list as a Number and also append a computed Integer to that same list. Which parameter type can it use?
- List<? extends Number>, because it already guarantees the elements are Numbers
- List<? super Integer>, because it already allows Integers to be added
- List<Number>, because no single wildcard can serve as both producer and consumer
- List<?>, because the unknown type covers reading and writing equally
Show answer
Reading as Number requires the element type to be at most Number; adding an Integer requires it to be at least Integer. A wildcard fixes only one of those directions, so the parameter has to name an exact type such as List<Number>. List<? extends Number> is tempting because the reads work, but every add is rejected since the compiler cannot tell whether the argument is really a List<Double>.