Skip to content

Java · Generics

Java Generics — Wildcards, Bounded Types & the PECS Principle

9 min read Updated 2026-06-20 Share:

Practice Wildcards & Bounded Types interview questions

Why wildcards exist

Generics give you compile-time type safety, but they come with a surprise that trips up almost everyone: List<Integer> is not a List<Number>. Generics are invariant, and that single fact is the reason wildcards exist. A method that wants to work over a family of related parameterizations — "any list of numbers," "any collection I can pour integers into" — needs a way to express that without sacrificing safety. Wildcards (?) and bounded type parameters (<T extends ...>) are the two tools for the job, and knowing which to reach for is most of what generics interviews are really testing.

The unbounded wildcard

The simplest wildcard is <?>, read as "a list of some unknown type." You use it when the method body genuinely doesn't care about the element type — it treats everything as Object. It is the type-safe replacement for a raw type: a raw List disables type checking entirely, while List<?> keeps the compiler honest.

static void printAll(List<?> list) {          // any element type
  for (Object o : list) System.out.println(o); // safe: everything is an Object
  // list.add("x");                            // compile error — unknown element type
}
printAll(List.of(1, 2, 3));   // List<Integer> ok
printAll(List.of("a", "b"));  // List<String> ok

Because the element type is hidden, an unbounded wildcard list is effectively read-only: you can read elements as Object, but the only thing you can add is null. Don't confuse List<?> with List<Object> — the latter is a single concrete type that matches only List<Object>, whereas List<?> matches every parameterization.

Upper bounds: producers

An upper-bounded wildcard <? extends T> means "some unknown subtype of T." It shines when you want to read T values out of a structure. Every element is guaranteed to be at least a T, so reading as T is always safe — this is the producer side.

static double sum(List<? extends Number> nums) { // Number or any subtype
  double total = 0;
  for (Number n : nums) total += n.doubleValue(); // safe: all are Numbers
  return total;
}
sum(List.of(1, 2, 3));   // List<Integer> ok
sum(List.of(1.5, 2.5));  // List<Double> ok

What you can't do is add to a List<? extends Number>. The compiler knows the list is some subtype of Number but not which one — it could really be a List<Integer> or a List<Double> — so no concrete value is provably safe to insert. The list reads, it does not write. The rule: extends is for getting, not putting.

Lower bounds: consumers

The mirror image is the lower-bounded wildcard <? super T>, "some unknown supertype of T." This is what you want when you need to write T values into a structure. Whatever the real type is, it is T or wider, so a T always fits — the consumer side.

static void addNumbers(List<? super Integer> list) { // Integer or any supertype
  list.add(1);                                        // safe — an Integer fits
  list.add(2);
  Object o = list.get(0);                             // reads come back as Object only
}
addNumbers(new ArrayList<Integer>()); // ok
addNumbers(new ArrayList<Number>());  // ok — Integer fits a Number list
addNumbers(new ArrayList<Object>());  // ok — Integer fits an Object list

Reading from a super wildcard gives you back only Object, because the widest type common to every supertype of T is Object. So super wildcards are for putting in; the type you read out is untyped. That asymmetry — extends reads, super writes — is the whole game.

PECS: Producer Extends, Consumer Super

The mnemonic that ties it together is PECS: Producer Extends, Consumer Super. When a parameter produces Ts for you to read, use <? extends T>; when it consumes Ts you supply, use <? super T>. The canonical demonstration is a copy method, where the source produces and the destination consumes.

// src PRODUCES elements -> extends; dest CONSUMES elements -> super
static <T> void copy(List<? extends T> src, List<? super T> dest) {
  for (T t : src) dest.add(t);   // read from src, write to dest
}
List<Integer> ints = List.of(1, 2, 3);
List<Number>  dst  = new ArrayList<>();
copy(ints, dst);                 // Integer producer -> Number consumer

This is exactly how Collections.copy is declared, and the payoff is API flexibility: callers can pass a wider range of list types on each side than a rigid List<T> would allow. You see the same idea throughout the standard library — Collection.addAll takes Collection<? extends E> because the argument is a producer it only reads from.

Bounded type parameters vs wildcards

Wildcards aren't the only way to constrain a type. A bounded type parameter<T extends Number> also restricts T to Number or below — so when do you pick which? The deciding question is whether you need to name the type. A type parameter has a name you can reuse; a wildcard does not.

// type parameter: the SAME T links the input and the return type
static <T extends Number> T firstOf(List<T> list) { return list.get(0); }
Integer i = firstOf(List.of(1, 2)); // returns Integer, not just Number

// wildcard: the type is used once and never named
static double sumOf(List<? extends Number> list) { /* ... */ return 0; }

Use a type parameter when the type appears more than once — relating two arguments, or flowing back through the return type. Use a wildcard when the type shows up exactly once and you never need to refer to it. A wildcard has no name, so the most a method can return from List<? extends Number> is a plain Number; if the output must match the caller's exact element type, you need the named parameter.

Multiple and recursive bounds

A type parameter can demand several bounds at once with &, meaning T must satisfy all of them. At most one bound may be a class, and if present it must come first; the rest must be interfaces. Wildcards can carry only a single bound, so this is type-parameter-only territory.

// T must be BOTH Comparable AND Serializable
static <T extends Comparable<T> & Serializable> T maxOf(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;   // Comparable methods available on T
}

A special case you'll see constantly is the recursive bound <T extends Comparable<T>> — "T must be comparable to itself." It captures the notion "this type knows how to order its own instances," which is precisely what sorting and min/max require; without the recursion you couldn't safely pass a T to compareTo.

static <T extends Comparable<T>> T max(List<T> list) {
  T best = list.get(0);
  for (T t : list)
    if (t.compareTo(best) > 0) best = t; // t.compareTo(T) is type-safe
  return best;
}

The real Collections.max loosens this to <T extends Comparable<? super T>> — PECS again, so a subtype can reuse an ancestor's compareTo instead of redeclaring its own.

Wildcard capture

Sometimes you have a <?> signature but the method body needs to name that unknown type. The compiler assigns it a temporary name (you'll see CAP#1 in errors), but it won't let you, say, take an element out of a List<?> and put it back in — it can't prove the two ends match. The fix is the capture-helper pattern: a private generic method that captures the wildcard into a real type variable.

// public method keeps the clean <?> signature
static void swap(List<?> list, int i, int j) {
  swapHelper(list, i, j);                 // delegate to capture the wildcard
}
private static <T> void swapHelper(List<T> list, int i, int j) {
  T tmp = list.get(i);                    // now there is a concrete T
  list.set(i, list.get(j));
  list.set(j, tmp);
}

The helper's T captures the wildcard, so inside it the get-and-set both type-check while callers still see the tidy List<?> API.

Array covariance vs generic invariance

Why is any of this necessary? Because Java made opposite choices for arrays and generics. Arrays are covariantString[] is an Object[] — which feels convenient but defers safety to runtime. Generics are invariantList<String> is not a List<Object> — catching the mistake at compile time instead.

Object[] arr = new String[3];
arr[0] = 42;                                  // compiles, throws ArrayStoreException at RUNTIME

List<Object> list = new ArrayList<String>();  // compile error — caught early

Generics chose invariance because erasure removes type info at runtime — there is no ArrayStore check to fall back on, so the unsafe assignment is forbidden up front. Wildcards then hand the lost flexibility back, safely: List<? extends Number> accepts a List<Integer> for reading without ever letting you insert the wrong element. They are the opt-in bridge between strict invariance and the covariance (extends) or contravariance (super) you actually want — applied exactly where it's provably sound.

Recap

Generics are invariant by design, and wildcards restore the flexibility that invariance costs. Use <?> when the element type is irrelevant; use <? extends T> for producers you read from and <? super T> for consumers you write to — the PECS rule, "Producer Extends, Consumer Super." Reach for a bounded type parameter instead of a wildcard whenever you must name the type to reuse it or return it, and lean on multiple bounds (<T extends A & B>) and recursive bounds (<T extends Comparable<T>>) when one constraint isn't enough. Remember the get-and-put principleextends to get, super to put, an exact type to do both — keep the capture-helper pattern in your back pocket, and you'll read the standard library's gnarly signatures, and write your own, with confidence.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel