Ch. 7 · Java

Java Generics Wildcards and PECS

Why List<Integer> isn't a List<Number>, what ? extends and ? super let you read and write, and how PECS shapes real JDK method signatures.

~7 min readadvancedupdated Oct 19, 2026

Java generics questions in interviews tend to start innocently (“why doesn’t this compile?”) and finish with the interviewer asking you to explain the signature of Collections.copy or Stream.map. The bridge between the two is wildcards: ?, ? extends T and ? super T. Most developers can recognise them, fewer can say exactly what each allows, and fewer still can decide which to use when designing a method.

This guide starts from the compile error everyone hits, explains why Java forbids it, then shows what each wildcard permits and why, using the memory aid PECS. It ends with how to read real JDK signatures and how to design your own.

Note

No JDK was available while writing this, so the Java snippets are illustrative and were not compiled for this article. They follow standard Java 17+ rules, and compiler messages are quoted in the form javac typically prints them; exact wording varies a little between versions.

Why a List of Integer isn’t a List of Number

Start with a method that sums numbers:

static double sum(List<Number> values) {
    double total = 0;
    for (Number n : values) total += n.doubleValue();
    return total;
}

List<Integer> scores = List.of(90, 85, 77);
sum(scores);
// error: incompatible types: List<Integer> cannot be converted to List<Number>
java

Integer is a Number, so this looks like it should work. It doesn’t, because generic types in Java are invariant: List<Integer> and List<Number> are unrelated types, regardless of how Integer and Number are related.

The reason becomes obvious when you imagine the opposite rule. Suppose the compiler allowed it:

List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> nums = ints;        // pretend this compiles
nums.add(3.14);                  // a Double is a Number, so this is fine for List<Number>
Integer first = ints.get(3);     // ClassCastException: Double cannot be cast to Integer
java

nums and ints are the same list object. Through the List<Number> view you could add a Double, and then code holding the List<Integer> view would get a Double out of what it believes is a list of integers. Invariance prevents that at compile time.

Arrays made the other choice

Java arrays are covariant: Integer[] is a subtype of Number[]. So the equivalent array code compiles, and fails at runtime instead:

Integer[] ints = {1, 2, 3};
Number[] nums = ints;            // compiles
nums[0] = 3.14;                  // throws ArrayStoreException at runtime
java

Arrays carry their element type at runtime and check every store, so the JVM catches the problem, just later than you’d like. Generics are erased at runtime (a List<Integer> is just a List to the JVM), so no such runtime check is possible, and the compiler has to prevent it up front. That contrast between arrays and generics is a frequent follow-up question.

? extends T: a producer you can read from

To write sum so it accepts a list of any kind of number, use an upper-bounded wildcard:

static double sum(List<? extends Number> values) {
    double total = 0;
    for (Number n : values) total += n.doubleValue();
    return total;
}

sum(List.of(90, 85, 77));        // List<Integer>: fine
sum(List.of(1.5, 2.5));          // List<Double>: fine
java

List<? extends Number> means “a list of some specific type that is Number or a subclass, but I don’t know which”. List<Integer>, List<Double> and List<Number> all fit.

Reading is safe. Whatever the unknown type is, every element is at least a Number, so Number n = values.get(0) always type-checks.

Writing isn’t allowed:

static void addDefault(List<? extends Number> values) {
    values.add(0);
    // error: incompatible types: int cannot be converted to CAP#1
}
java

The compiler doesn’t know whether the list is a List<Integer>, a List<Double> or something else. Adding an Integer would be wrong if it’s a list of doubles, and adding a Double would be wrong if it’s a list of integers. Since no type is safe for every possibility, nothing can be added (except null, which belongs to every reference type). CAP#1 in the error is the compiler’s name for the unknown captured type.

So ? extends gives you a read-only view in terms of the element type. Removing elements and calling clear() still work, since neither needs to know the type. “Read-only” refers to what you can put in, not to immutability.

? super T: a consumer you can write to

The lower-bounded wildcard is the mirror image. Here’s a method that fills a list with integers:

static void addRange(List<? super Integer> target, int from, int to) {
    for (int i = from; i <= to; i++) target.add(i);
}

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
addRange(ints, 1, 3);
addRange(nums, 1, 3);
addRange(objs, 1, 3);
java

List<? super Integer> means “a list of some type that is Integer or a supertype of it”. It could be a List<Integer>, List<Number> or List<Object>, and every one of those can safely hold an Integer. So adding an Integer is always allowed.

Reading gets less useful. An element might be an Integer, but in a List<Object> it could equally be a String someone added earlier. The only type the compiler can promise is Object:

Object o = target.get(0);        // fine
Integer i = target.get(0);       // error: incompatible types: CAP#1 cannot be converted to Integer
java

PECS: deciding which wildcard to use

Joshua Bloch’s Effective Java gives the rule as a mnemonic: Producer Extends, Consumer Super. Ask what a parameter does for your method:

  • If the method takes values out of it (the parameter produces T for you), use ? extends T.
  • If the method puts values into it (the parameter consumes T from you), use ? super T.
  • If the method does both, use plain T, since no wildcard allows both safely.

The rule is about the parameter’s role from the method’s point of view. A Comparator<T> takes Ts as arguments to compare, so it consumes them and should be Comparator<? super T>, even though nothing gets “added” to it. That’s why List.sort is declared as sort(Comparator<? super E> c): a Comparator<Number> can sort a List<Integer>, because anything that can compare two numbers can compare two integers.

Reading JDK signatures

Collections.copy(List<? super T> dest, List<? extends T> src): the source produces Ts and the destination consumes them. You can copy a List<Integer> into a List<Number>, with T inferred as Integer.

Stream<T>.map(Function<? super T, ? extends R> mapper): the function consumes the stream’s elements (so it can accept any supertype of T) and produces results (so it can return any subtype of R). A Function<Object, String> works for mapping a Stream<Integer> to a Stream<CharSequence>.

Collections.max(Collection<? extends T> coll) with the bound <T extends Object & Comparable<? super T>>: the collection produces elements, and T must be comparable to itself or to a supertype. The ? super T there lets you find the max of a List<java.sql.Date>, even though java.sql.Date inherits Comparable<java.util.Date> from its parent rather than implementing Comparable<java.sql.Date>. The Object & part is there for binary compatibility with the pre-generics version of the method, a detail interviewers rarely expect but sometimes enjoy.

Unbounded ? and wildcard capture

List<?> means a list of some unknown type. It’s the right choice when you don’t care about the element type at all, such as a method that prints a list’s size or checks isEmpty(). It differs from List<Object>: a List<String> can be passed as a List<?> but not as a List<Object>, by the same invariance rule as before.

Occasionally you need to write to a List<?> using its own elements, for example to swap two of them. You can’t call list.set(i, list.get(j)) directly, because the compiler treats the two unknowns as unrelated. The standard trick is a private generic helper that captures the type:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

private static <E> void swapHelper(List<E> list, int i, int j) {
    E tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}
java

Inside the helper, the unknown type has a name, E, so reading and writing it type-checks. Collections.swap in the JDK is also declared with List<?> to keep its public signature simple, although its implementation takes a shortcut with a raw List instead of a helper.

Mistakes interviewers listen for

The most common slip is putting wildcards in return types. A method returning List<? extends Number> forces every caller to deal with a type they can’t add to and can’t name. Return a concrete type such as List<Number> or List<T>, and keep wildcards for parameters, where they make the API accept more inputs.

Another is using ? extends on a parameter the method writes to, then working around the compile error with an unchecked cast. If you find yourself casting, the wildcard direction is probably wrong.

A third is thinking wildcards affect runtime behaviour. Because of erasure, List<? extends Number> and List<String> are both just List at runtime. That’s also why you can’t write new ArrayList<? extends Number>(); a wildcard describes a variable’s type, not an object you can create.

Practice question: design a pipeline method

Question. Write a signature for transferAll, which takes a source collection, a Predicate used to filter, and a destination collection. It should accept: a List<Integer> source, a Predicate<Number> that keeps positive values, and a List<Object> destination. Justify each wildcard.

Answer. Introduce a type parameter for the element type and apply PECS to each argument:

static <T> void transferAll(Collection<? extends T> source,
                            Predicate<? super T> keep,
                            Collection<? super T> dest) {
    for (T item : source) {
        if (keep.test(item)) dest.add(item);
    }
}
java

The source produces elements, so it’s ? extends T. The predicate consumes elements as arguments to test, so it’s ? super T, which is what lets a Predicate<Number> filter integers. The destination consumes elements via add, so it’s ? super T, which allows a List<Object>. With the call described, the compiler infers T as Integer: the source is exactly List<Integer>, Number and Object are both supertypes of it, and everything checks. If you’d written all three parameters with plain T, the call would fail, because no single T equals Integer, Number and Object at once. A good answer finishes by noting that the method returns void, so no wildcard leaks into a return type, and that Predicate<? super T> mirrors the JDK’s own Collection.removeIf(Predicate<? super E>).

Further reading

More in Java

read ✓Java · easy

Checked vs Unchecked Exceptions in Java

Checked exceptions must be caught or declared; unchecked ones needn't be. See the compiler rules, the lambda problem and when to use each.

~7 min readread →
esc