Wildcards

Su Suriya Ravichandran Updated 14 Nov 2026
7 min read ·Lesson 90 of 133

A wildcard, written ?, stands for "some unknown type" in a generic type. Wildcards let a method accept a family of parameterized types instead of exactly one: a List<Integer>, a List<Double>, and a List<Number> can all be passed where List<? extends Number> is expected. They exist because generic types are deliberately not covariant, and without wildcards many perfectly reasonable methods could not be written in a type-safe way.

The Problem: Generics Are Invariant

Integer is a subtype of Number, but List<Integer> is not a subtype of List<Number>:

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

List<Integer> ints = List.of(1, 2, 3);
sum(ints);          // compile error: List<Integer> cannot be converted to List<Number>

This restriction is intentional. If a List<Integer> could be treated as a List<Number>, this would compile:

List<Integer> ints = new ArrayList<>(List.of(1, 2));
List<Number> nums = ints;          // imagine this were allowed
nums.add(3.14);                    // a Double inside a List<Integer>!
int x = ints.get(2);               // ClassCastException

Generics forbid the assignment so that this can never happen. Wildcards then allow the safe subset of such uses.

Upper-Bounded Wildcards: ? extends T

List<? extends Number> means "a list of some type that is Number or a subtype of it." Any of List<Number>, List<Integer>, List<Double>, or List<BigDecimal> matches.

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

sum(List.of(1, 2, 3));            // List<Integer>: 6.0
sum(List.of(1.5, 2.5));           // List<Double>: 4.0

You can read, but not add

Elements read from a List<? extends Number> are guaranteed to be Numbers, so reading is safe. Adding is not:

List<? extends Number> nums = new ArrayList<Integer>();
Number first = nums.get(0);        // OK: whatever it is, it is a Number
nums.add(42);                      // compile error
nums.add(3.14);                    // compile error
nums.add(null);                    // allowed, but rarely useful

The compiler knows the list holds some specific subtype of Number, but not which one. It might be a List<Double>, so adding an Integer could corrupt it. Therefore it allows no additions at all (except null).

Rule: use ? extends T for a parameter that the method only reads from, a "producer" of T values.

Lower-Bounded Wildcards: ? super T

List<? super Integer> means "a list of Integer or some supertype of it": List<Integer>, List<Number>, or List<Object>.

static void addDefaults(List<? super Integer> target) {
    target.add(0);
    target.add(100);
}

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();

addDefaults(ints);    // OK
addDefaults(nums);    // OK
addDefaults(objs);    // OK

You can add, but reads are only Object

Whatever the actual list type is, it can hold an Integer, so adding Integers is safe. Reading gives only Object, because the list might be a List<Object> containing anything:

List<? super Integer> target = new ArrayList<Number>();
target.add(5);                 // OK
Object o = target.get(0);      // OK, but only as Object
Integer i = target.get(0);     // compile error

Rule: use ? super T for a parameter that the method only writes T values into, a "consumer" of T.

PECS: Producer Extends, Consumer Super

The mnemonic PECS summarizes the two rules:

Parameter roleWildcardCan read as TCan add T
Producer: the method takes values out of it? extends TYesNo
Consumer: the method puts values into it? super TNo (only Object)Yes
BothExact type TYesYes

The JDK applies PECS throughout. Collections.copy copies from a producer into a consumer:

public static <T> void copy(List<? super T> dest, List<? extends T> src)
List<Integer> src = List.of(1, 2, 3);
List<Number> dest = new ArrayList<>(List.of(0, 0, 0));
Collections.copy(dest, src);       // T = Integer: read Integers from src, write them into dest

The same idea appears in functional parameters. Predicate<? super T> and Consumer<? super T> consume T values, and Function<? super T, ? extends R> consumes a T and produces an R:

List<Integer> scores = List.of(45, 82, 67);
Predicate<Object> notNull = o -> o != null;
scores.stream().filter(notNull).count();          // filter takes Predicate<? super Integer>

Consumer<Object> logger = o -> System.out.println("log: " + o);
scores.forEach(logger);                           // forEach takes Consumer<? super Integer>

A general-purpose Predicate<Object> or Consumer<Object> can be reused for any element type, because a function that accepts any Object certainly accepts an Integer.

Unbounded Wildcards: ?

List<?> means "a list of some unknown type." It is useful when the method does not care about the element type at all, only about the list's structure:

static void printAll(List<?> items) {
    System.out.println(items.size() + " item(s): " + items);
}

printAll(List.of(1, 2));
printAll(List.of("a", "b"));

Elements can be read only as Object, and nothing except null can be added. Typical uses are methods that rely only on Object methods (size, isEmpty, toString, equals), or on Class<?> when the exact class is unknown.

List<?> versus List

These are not the same:

List<Object>List<?>
Accepts a List<String> argumentNoYes
Can add elementsYes (any object)No (only null)
MeaningA list that may hold any mix of objectsA list of one specific, unknown type

A List<Object> parameter accepts only an actual List<Object>. A List<?> parameter accepts any list.

Wildcards Versus Type Parameters

Many signatures can be written either way:

static void printAll(List<?> items)            // wildcard
static <T> void printAll(List<T> items)        // type parameter

Prefer a wildcard when the type is used in only one place and the method does not need to refer to it. Use a type parameter when the same type must appear in several places, linking them together:

static <T> void swapFirstTwo(List<T> list) {   // needs T: reads one element and writes it back
    T tmp = list.get(0);
    list.set(0, list.get(1));
    list.set(1, tmp);
}

With List<?>, the set calls would not compile, because the compiler could not prove the values being written match the list's unknown type.

Where wildcards are allowed:

  • In parameter types, field types, local variable types, and return types (though wildcards in return types are usually awkward for callers).
  • Not in new expressions (new ArrayList<?>() is illegal), in a class's extends/implements clause, or as an explicit type argument to a generic method call.

Practical Example: Processing Payments

A payments module has several payment types. Reporting code should work with a list of any one of them, and routing code should move matching payments into any suitable collection.

import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;

public class Wild {
    static abstract class Payment {
        final double amount;
        Payment(double amount) { this.amount = amount; }
        abstract String kind();
        @Override public String toString() { return kind() + " " + amount; }
    }
    static class CardPayment extends Payment {
        CardPayment(double a) { super(a); }
        String kind() { return "card"; }
    }
    static class UpiPayment extends Payment {
        UpiPayment(double a) { super(a); }
        String kind() { return "upi"; }
    }

    static double total(Collection<? extends Payment> payments) {
        double sum = 0;
        for (Payment p : payments) sum += p.amount;
        return sum;
    }

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

    static void printAll(List<?> items) {
        System.out.println(items.size() + " item(s): " + items);
    }

    public static void main(String[] args) {
        List<CardPayment> cards = List.of(new CardPayment(1200), new CardPayment(300));
        List<UpiPayment> upi = List.of(new UpiPayment(450), new UpiPayment(2500));

        System.out.println("Card total: " + total(cards));
        System.out.println("UPI total:  " + total(upi));

        List<Payment> review = new ArrayList<>();
        Predicate<Payment> large = p -> p.amount >= 1000;
        moveMatching(cards, large, review);
        moveMatching(upi, large, review);
        printAll(review);

        List<Object> audit = new ArrayList<>();
        moveMatching(upi, p -> true, audit);
        printAll(audit);

        Consumer<Object> logger = o -> System.out.println("log: " + o);
        cards.forEach(logger);
    }
}

Output:

Card total: 1500.0
UPI total:  2950.0
2 item(s): [card 1200.0, upi 2500.0]
2 item(s): [upi 450.0, upi 2500.0]
log: card 1200.0
log: card 300.0

Each wildcard matches the parameter's role:

  • total reads only. It takes Collection<? extends Payment>, so a List<CardPayment> and a List<UpiPayment> are both accepted. With Collection<Payment>, neither call would compile.
  • moveMatching follows PECS. The source is a producer (? extends T), and the target is a consumer (? super T). The predicate also consumes values (? super T), which is why a single Predicate<Payment> filters both cards and UPI payments.
  • The target can be any suitable collection. Matching payments go into a List<Payment> for review or a List<Object> for an audit log.
  • printAll uses List<?>. It only needs size() and toString(), so it accepts any list.
  • forEach takes Consumer<? super CardPayment>. That is why a general Consumer<Object> logger works for card payments.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Wildcards and get answers drawn from it.

Signed-in readers only.