Back to Blog
Java

Java Wildcard: Using ?, extends, and super in Generics

java wildcard: Explains the three Java wildcard forms — ?, ? extends, and ? super — with the PECS rule, wildcard capture, and type erasure behavior.

Java GenericsWildcardsPECSType SafetyJava Collections
Diagram showing three Java wildcard forms and how they constrain reading and writing elements in a generic collection.

Wildcards in Java generics let you write a method that accepts a family of related types instead of a single fixed type. The java wildcard syntax — ?, ? extends T, and ? super T — appears constantly in library APIs, collection utilities, and generic method signatures. Understanding what each form permits and forbids is the difference between code that compiles cleanly and code that fails with confusing "capture of ? extends" errors.

The Three Wildcard Forms and What They Permit

A wildcard is a question mark that stands for an unknown type argument. Java supports three forms:

FormSyntaxMeaning
UnboundedList<?>A list of some unknown type
Upper-boundedList<? extends Number>A list of some type that is Number or a subtype
Lower-boundedList<? super Integer>A list of some type that is Integer or a supertype

The distinction matters because the compiler applies different rules to each form. An unbounded wildcard accepts any type argument, but it also forbids most operations that depend on knowing the element type. An upper-bounded wildcard allows you to read elements as the bound type but prevents you from adding elements. A lower-bounded wildcard allows you to add elements of the bound type but restricts what you can safely read.

Unbounded Wildcards for Read-Only Access

List<?> is useful when a method only needs to inspect a collection without caring about the element type. The classic example is a method that counts elements or checks emptiness:

public static int countElements(List<?> items) { return items.size(); }

Because the element type is unknown, you cannot call items.add(something) — the compiler has no way to verify that something matches the unknown type. You can, however, iterate and read elements as Object:

public static void printAll(List<?> items) { for (Object item : items) { System.out.println(item); } }

The practical rule is that List<?> is for methods that only read or pass the collection through. If a method needs to add elements, an unbounded wildcard is the wrong choice.

Upper-Bounded Wildcards for Reading a Family of Types

? extends T is the form most developers reach for when they want to accept a collection whose element type is T or any subtype. A method that sums numbers can accept List<Integer>, List<Double>, or List<Number>:

public static double sumNumbers(List<? extends Number> numbers) { double total = 0.0; for (Number number : numbers) { total += number.doubleValue(); } return total; }

The compiler lets you read each element as Number, because every element is guaranteed to be a Number or a subtype. What it forbids is adding elements. You cannot call numbers.add(new Integer(5)) because the actual type argument could be List<Double>, and adding an Integer would break type safety.

This asymmetry is the source of most wildcard confusion: an upper-bounded wildcard is safe to read from but unsafe to write to.

Lower-Bounded Wildcards for Writing Elements

? super T is the mirror image. It accepts a collection whose element type is T or any supertype of T. This is the form used by Collections.addAll-style methods that push elements into a destination collection:

public static void addIntegers(List<? super Integer> destination) { destination.add(1); destination.add(2); }

The method can be called with List<Integer>, List<Number>, or List<Object>, because each of those can safely hold an Integer. Reading is the problem: when you read from a List<? super Integer>, the element type is unknown above Integer, so you can only treat elements as Object.

Lower-bounded wildcards appear less often in application code than upper-bounded ones, but they are essential when writing generic collection utilities or when a method must populate a collection.

The PECS Rule: Producer Extends, Consumer Super

The mnemonic PECS — Producer Extends, Consumer Super — tells you which wildcard to use based on the direction of data flow. If a method only produces values from a collection, use ? extends T. If a method only consumes values into a collection, use ? super T.

Consider a method that copies elements from a source list into a destination list:

public static <T> void copy(List<? extends T> source, List<? super T> destination) { for (T item : source) { destination.add(item); } }

The source is a producer, so it is declared with ? extends T. The destination is a consumer, so it is declared with ? super T. This signature is exactly what Collections.copy uses. Getting the direction wrong produces a method that either cannot read its input or cannot write its output.

The rule is not a style preference. It reflects the compiler's type-safety constraints: an upper-bounded wildcard guarantees readable values of the bound type, and a lower-bounded wildcard guarantees writable values of the bound type.

Wildcard Capture and the Compiler's Hidden Type Variable

When the compiler processes a wildcard, it internally treats the unknown type as a fresh type variable. This is called wildcard capture. It explains why a generic method can often do things that a wildcard method cannot.

A common failure is trying to swap two elements in a List<?>:

public static void swap(List<?> list, int i, int j) { // Does not compile Object temp = list.get(i); list.set(j, temp); }

The compiler rejects the set call because it cannot prove that temp, an Object, matches the unknown element type. The fix is to introduce a helper method that captures the wildcard in a type variable:

public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } private static <T> void swapHelper(List<T> list, int i, int j) { T temp = list.get(i); list.set(j, temp); }

Inside swapHelper, T is a concrete type variable, so the read and write are type-safe. This pattern — a public wildcard method delegating to a private generic helper — is the standard way to work around capture limitations.

Runtime Behavior: Type Erasure and Why Wildcards Cost Nothing

Wildcards exist only at compile time. The Java compiler erases all type arguments, including wildcards, when it generates bytecode. A List<? extends Number> and a List<Number> both compile to the same erased type, List, with the element type erased to Number for the upper bound.

The practical consequence is that wildcards have no runtime cost. There is no boxing overhead, no reflection, and no additional object allocation caused by using ? instead of a concrete type. The only cost is compile-time checking, which is precisely the point: wildcards push type-safety errors to the compiler instead of allowing them to surface as ClassCastException at runtime.

One runtime consideration does apply: because erasure removes the element type, a List<?> cannot be used to create typed arrays or to perform unchecked casts safely. Code that mixes wildcards with raw types, such as casting a raw List to List<? extends Number>, may compile with an unchecked warning and fail at runtime. Keeping wildcards inside clean generic code avoids that hazard.

Common Wildcard Mistakes and How to Fix Them

The most frequent mistake is using ? extends T for a collection that must be written to. A method that fills a list with default values cannot use an upper-bounded wildcard:

public static void fillDefaults(List<? extends Integer> list) { // Does not compile list.add(0); }

The fix is to use ? super Integer or a concrete type parameter. The same error appears in reverse when a method tries to read concrete values from a ? super T collection.

A second mistake is overusing unbounded wildcards in method signatures that need to return elements. A method declared as returning List<?> forces every caller to cast the result, which defeats the type safety wildcards are meant to provide. Prefer a type parameter when the method must return a value of the element type.

A third mistake is ignoring the capture problem and reaching for an unsafe cast. Casting a List<?> to List<String> to call a mutating method suppresses the compiler's checks and can produce a runtime ClassCastException when the actual element type differs. The helper-method pattern is safer and costs nothing at runtime.

Choosing between ? extends T, ? super T, and a concrete type parameter comes down to the direction of data flow. Read-only access from a family of types calls for ? extends T. Write-only access into a family of types calls for ? super T. When a method both reads and writes, or when it must return the element type, a type parameter is usually the clearer choice.

java wildcard: Practical Usage and Code Examples | RYUSLOG DEV