Wildcards
Wildcards — written as ? — represent an unknown type in a generic type argument position. Where a type parameter T is a name given to an unknown type that can be referenced throughout a declaration, a wildcard ? is an anonymous unknown type used at the point of consumption. Wildcards appear in method parameter types, field types, return types, and variable declarations — anywhere a parameterized type appears as a type (not where a type is being declared). The three wildcard forms are: the unbounded wildcard (List<?>), the upper-bounded wildcard (List<? extends Number>), and the lower-bounded wildcard (List<? super Integer>). Wildcards are the mechanism that introduces covariance and contravariance into Java's otherwise invariant generic type system. This entry covers all three forms in depth, the producer-extends consumer-super (PECS) principle, wildcard capture and the capture helper pattern, nested wildcards, and when to use wildcards vs type parameters.
Unbounded Wildcard — List<?> and What It Means
// ── Unbounded wildcard — accepts any List ────────────────────────────
public static void printList(List<?> list) {
for (Object element : list) { // element is Object — unknown type
System.out.println(element);
}
}
printList(List.of("alpha", "beta", "gamma")); // OK — List<String>
printList(List.of(1, 2, 3, 4, 5)); // OK — List<Integer>
printList(List.of(3.14, 2.71)); // OK — List<Double>
// ── Cannot add typed elements to List<?> ─────────────────────────────
List<?> unknown = new ArrayList<>(List.of("a", "b", "c"));
// unknown.add("hello"); // COMPILE ERROR — ? might be List<Integer>
// unknown.add(42); // COMPILE ERROR
unknown.add(null); // OK — null is always compatible
// Reading returns Object only:
Object first = unknown.get(0); // OK — Object
// String s = unknown.get(0); // COMPILE ERROR — get() returns Object, not String
// ── List<?> vs List<Object> — the critical difference ─────────────────
List<String> strings = List.of("a", "b");
List<Object> objects = List.of("a", 42); // List<Object> can hold mixed types
// List<String> IS a subtype of List<?>:
List<?> wild = strings; // OK — covariance via wildcard
// List<String> is NOT a subtype of List<Object>:
// List<Object> objs = strings; // COMPILE ERROR — generics are invariant
// List<Object> accepts ONLY List<Object> or raw List:
// List<Object> objs2 = List.of("a", "b"); // COMPILE ERROR
// ── Unbounded wildcard is appropriate when you only need List operations:
public static int countNonNull(List<?> list) {
int count = 0;
for (Object element : list) {
if (element != null) count++;
}
return count;
}
public static boolean hasMoreThan(List<?> list, int n) {
return list.size() > n; // .size() doesn't care about element type
}
public static void swap(List<?> list, int i, int j) {
// Cannot call list.set(i, list.get(j)) — get() returns Object, set() needs ?
// Must use capture helper (see wildcard capture section):
swapHelper(list, i, j);
}
// Capture helper resolves the unknown type to a named parameter:
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
List<String> data = new ArrayList<>(List.of("a", "b", "c", "d"));
swap(data, 0, 3);
System.out.println(data); // [d, b, c, a]
// ── Raw type vs wildcard — always prefer wildcard ─────────────────────
List raw = new ArrayList(); // raw — unchecked, unsafe
raw.add("hello");
raw.add(42); // no compile error — all type checking disabled
List<?> wild2 = new ArrayList<>(); // wildcard — type-safe
// wild2.add("hello"); // COMPILE ERROR — type checking enforcedUpper-Bounded Wildcard — ? extends T
// ── Upper-bounded wildcard — reading is typed, writing is forbidden ────
public static double sumList(List<? extends Number> list) {
double sum = 0;
for (Number element : list) { // get() returns Number — not just Object
sum += element.doubleValue(); // Number methods available
}
return sum;
}
// All three work — covariance via ? extends:
System.out.println(sumList(List.of(1, 2, 3, 4, 5))); // 15.0 (List<Integer>)
System.out.println(sumList(List.of(1.1, 2.2, 3.3))); // 6.6 (List<Double>)
System.out.println(sumList(List.of(1L, 2L, 3L))); // 6.0 (List<Long>)
System.out.println(sumList(List.<Number>of(1, 2.0, 3L))); // 6.0 (List<Number>)
// WITHOUT wildcard — only List<Number> accepted:
public static double sumListNoWildcard(List<Number> list) { /* ... */ }
// sumListNoWildcard(List.of(1, 2, 3)); // COMPILE ERROR — List<Integer> ≠ List<Number>
// ── Cannot add to ? extends ───────────────────────────────────────────
List<? extends Number> numbers = new ArrayList<>(List.of(1, 2, 3));
// numbers.add(42); // COMPILE ERROR — might be List<Double>
// numbers.add(3.14); // COMPILE ERROR — might be List<Integer>
numbers.add(null); // OK — null is always safe
// Reading is fine and typed:
Number first = numbers.get(0); // Number — can call any Number method
double d = first.doubleValue();
// ── Covariance in practice ────────────────────────────────────────────
List<Integer> ints = List.of(10, 20, 30);
List<Double> doubles = List.of(1.1, 2.2, 3.3);
// Both assignable to List<? extends Number>:
List<? extends Number> n1 = ints; // OK
List<? extends Number> n2 = doubles; // OK
// ── Upper bound in method parameters ─────────────────────────────────
public static Number findMax(List<? extends Number> list) {
if (list.isEmpty()) throw new NoSuchElementException();
Number max = list.get(0);
for (Number n : list) {
if (n.doubleValue() > max.doubleValue()) max = n;
}
return max;
}
System.out.println(findMax(List.of(3, 1, 4, 1, 5))); // 5 (Integer)
System.out.println(findMax(List.of(1.1, 9.9, 2.2))); // 9.9 (Double)
// ── Nested upper-bounded wildcard ────────────────────────────────────
// List of lists of numbers — each inner list can be any number subtype:
public static void printNestedSums(List<? extends List<? extends Number>> matrix) {
for (List<? extends Number> row : matrix) {
System.out.println(sumList(row)); // reuse sumList defined above
}
}
List<List<Integer>> intMatrix = List.of(
List.of(1, 2, 3),
List.of(4, 5, 6));
printNestedSums(intMatrix); // 6.0 / 15.0
// ── Upper bound with generic method type parameter ────────────────────
// Sometimes a generic method is cleaner than a wildcard:
// Wildcard version — cannot refer to element type in return:
public static void copyAll(List<? extends Number> src, List<Number> dest) {
dest.addAll(src);
}
// Type parameter version — can express relationship between src and dest:
public static <T extends Number> void copyTyped(List<T> src, List<? super T> dest) {
dest.addAll(src); // PECS: src produces T, dest consumes T
}Lower-Bounded Wildcard, PECS, and Wildcard Capture
// ── Lower-bounded wildcard — writing is typed, reading is Object ───────
public static void fillWithIntegers(List<? super Integer> list, int count) {
for (int i = 0; i < count; i++) {
list.add(i); // OK — Integer fits in ? super Integer
}
}
List<Integer> intList = new ArrayList<>();
List<Number> numList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
fillWithIntegers(intList, 3); // List<Integer> — Integer supertype of itself
fillWithIntegers(numList, 3); // List<Number> — Number is supertype of Integer
fillWithIntegers(objList, 3); // List<Object> — Object is supertype of Integer
System.out.println(intList); // [0, 1, 2]
System.out.println(numList); // [0, 1, 2]
System.out.println(objList); // [0, 1, 2]
// COMPILE ERROR — List<Double> is not ? super Integer:
// fillWithIntegers(new ArrayList<Double>(), 3);
// Reading returns only Object:
List<? super Integer> consumer = numList;
Object o = consumer.get(0); // only Object guaranteed
// Integer i = consumer.get(0); // COMPILE ERROR — might be List<Number>
// ── PECS — Producer Extends, Consumer Super ───────────────────────────
// copy: src produces T, dest consumes T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T element : src) { // reading from src — extends (producer)
dest.add(element); // writing to dest — super (consumer)
}
}
List<Integer> source = List.of(1, 2, 3, 4, 5);
List<Number> destination = new ArrayList<>();
copy(destination, source); // T = Integer; dest=List<Number>, src=List<Integer>
System.out.println(destination); // [1, 2, 3, 4, 5]
// ── PECS with Comparator — consumer of T ─────────────────────────────
// Comparator<? super T>: the comparator consumes T values to compare them
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
list.sort(comparator);
}
List<String> words = new ArrayList<>(List.of("banana", "apple", "cherry"));
// Comparator<Object> can sort a List<String> — Object is supertype of String:
sort(words, Comparator.comparingInt(Object::hashCode));
// Natural order comparator is Comparator<String> — exact match also works:
sort(words, Comparator.naturalOrder());
System.out.println(words); // [apple, banana, cherry]
// ── Wildcard capture — helper method pattern ──────────────────────────
// Cannot swap elements of List<?> directly — get() returns Object, set() needs ?
public static void swap(List<?> list, int i, int j) {
swapCapture(list, i, j); // delegate to helper that captures the type
}
// The wildcard is captured as T — now we can read and write the same type:
private static <T> void swapCapture(List<T> list, int i, int j) {
T temp = list.get(i); // T known — safe read
list.set(i, list.get(j)); // T known — safe write
list.set(j, temp); // T known — safe write
}
List<String> letters = new ArrayList<>(List.of("a", "b", "c", "d"));
swap(letters, 0, 3);
System.out.println(letters); // [d, b, c, a]
// ── When wildcard vs type parameter ──────────────────────────────────
// USE wildcard when the type doesn't need to be referred to elsewhere:
public static void printSize(Collection<?> c) {
System.out.println(c.size()); // no need to name the element type
}
// USE type parameter when the type must be consistent across multiple uses:
public static <T> T getFirst(List<T> list, T defaultValue) {
return list.isEmpty() ? defaultValue : list.get(0);
// T must be the same type for both list's element and defaultValue
}
// USE type parameter when the return type must relate to the parameter:
public static <T> List<T> repeat(T value, int times) {
// List<T> returned — caller gets back the same type they provided
List<T> result = new ArrayList<>(times);
for (int i = 0; i < times; i++) result.add(value);
return result;
}