Skip to content

Wildcards & Bounded Types Interview Questions & Answers

21 questions Updated 2026-06-20 Share:

Java generics wildcards interview questions — unbounded, upper- and lower-bounded wildcards, the PECS principle, bounded type parameters, multiple bounds, and wildcard capture.

Read the in-depth guideJava Generics — Wildcards, Bounded Types & the PECS Principle(opens in new tab)
21 of 21

The unbounded wildcard <?> means "a List of some unknown type" — you don't know or care which. It makes a generic method work over collections of any element type while staying type-safe, unlike a raw type.

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

Because the element type is unknown, you can read elements as Object but can't add anything (except null) — the compiler can't prove your value matches the hidden type. Use <?> when the method body doesn't depend on the element type at all.

They look similar but behave very differently:

Declaration Accepts Can add? Type-safe?
List<?> any List<X> only null yes
List<Object> only List<Object> any object yes
List (raw) any List anything (unchecked) no — legacy
List<String> strs = new ArrayList<>();
List<?> a = strs;        // ok — unknown element type
List<Object> b = strs;   // compile error — not the same type
List c = strs;           // ok but unchecked warnings; defeats generics

List<Object> is a concrete type that only matches List<Object>. List<?> matches every parameterization but is read-only. The raw List exists only for pre-generics compatibility — it disables type checking, so avoid it.

<? extends T> means "some unknown subtype of T" — it sets an upper bound. It's used when you want to read T values out of a structure but don't need to know the exact subtype.

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

Every element is guaranteed to be at least a T, so reading as T is safe. This is the producer side — the structure produces Ts for you to consume.

Because the compiler knows the list is List<SomeSubtypeOfNumber> but not which subtype. It could be List<Integer>, List<Double>, or anything else — so adding any concrete value might violate the real element type.

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(1);        // compile error — what if it's really List<Double>?
nums.add(1.5);      // compile error — what if it's really List<Integer>?
nums.add(null);     // the only legal add — null fits any type
Number n = nums.get(0); // reading is fine — guaranteed to be a Number

The list becomes effectively read-only for element types. The rule: extends wildcards are for getting, not putting.

<? super T> means "some unknown supertype of T" — it sets a lower bound. It's used when you want to write T values into a structure.

static void addNumbers(List<? super Integer> list) { // Integer or any supertype
  list.add(1);          // safe — an Integer fits a List of Integer-or-wider
  list.add(2);
}
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

Whatever the real type is, it's Integer or wider, so an Integer is always a valid element. This is the consumer side — the structure consumes the Ts you supply.

Only Object. The compiler knows the list holds some supertype of T, but the widest guaranteed common type of any supertype is Object, so that's all a get can be typed as.

List<? super Integer> list = new ArrayList<Number>();
list.add(42);              // writing Integers is fine
Object o = list.get(0);    // ok — only Object is guaranteed
Integer i = list.get(0);   // compile error — could be a Number/Object list

So super wildcards are primarily for putting in; reading gives you back untyped Object. This asymmetry with extends is exactly what PECS captures.

PECS = Producer Extends, Consumer Super. When a parameter produces Ts for you (you read from it), use <? extends T>. When it consumes Ts you give it (you write to it), use <? super T>. The canonical example is a copy method:

// 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 how Collections.copy is actually declared. PECS maximizes API flexibility: callers can pass a wider range of list types on each side.

Use a bounded type parameter when you need to name the type — to refer to it more than once, relate two arguments, or use it in the return type. Use a wildcard when the type is used only once and you never need to name it.

// type parameter: the SAME T links input and return
static <T extends Number> T firstOf(List<T> list) { return list.get(0); }

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

A good rule: if a wildcard would force you to invent a helper just to name the type, use a type parameter instead. Otherwise prefer wildcards in API parameters — they read more cleanly to callers.

A type parameter can require several bounds joined by &, meaning T must satisfy all of them. At most one can be a class, and if present it must come first; the rest must be interfaces.

// 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; // can call Comparable methods on T
}

// class first, then interfaces:
static <T extends Number & Comparable<T>> T pick(T x) { return x; }

Within the method T exposes the members of all bounds. Wildcards, by contrast, allow only a single bound (<? extends A>), so multiple bounds require a named type parameter.

This is a recursive (self-referential) type boundT must be comparable to itself. It captures the idea "this type knows how to order its own instances," which is exactly what sorting and min/max need.

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;
}
max(List.of(3, 1, 2));        // T = Integer, Integer implements Comparable<Integer>

Without the recursion you couldn't safely call compareTo with a T argument. The real Collections.max uses <T extends Comparable<? super T>> — slightly looser so a subtype can reuse an ancestor's comparison.

Wildcard capture is the compiler giving the unknown type of a <?> a temporary name (shown in errors as CAP#1). Sometimes you need to name that type to operate on it — the fix is a private capture helper generic method that captures the wildcard into a real type variable.

// fails: compiler won't prove list.get(i) matches list.set(j, ...)
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 public method keeps the clean <?> signature; the helper captures the wildcard as T so the body type-checks.

Arrays are covariant: String[] is a Object[]. Generics are invariant: List<String> is not a List<Object>. Generics chose invariance for compile-time safety — array covariance defers the check to runtime and can blow up.

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

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

Because generics are erased at runtime there's no ArrayStore check to fall back on, so the language forbids the unsafe assignment up front. Wildcards then restore the lost flexibility safely (List<? extends Object> accepts a List<String> but won't let you put the wrong thing in).

Invariance means List<Integer> isn't a List<Number>, so a method taking List<Number> would reject an Integer list. A wildcard widens the parameter to accept a family of types while staying safe.

static double total(List<Number> list) { /* ... */ return 0; }
total(new ArrayList<Integer>());      // compile error — invariance

static double total2(List<? extends Number> list) { /* ... */ return 0; }
total2(new ArrayList<Integer>());     // ok — wildcard accepts subtypes
total2(new ArrayList<Double>());      // ok

So wildcards are the bridge between strict invariance and the polymorphism you actually want — you opt into covariance (extends) or contravariance (super) exactly where it's safe.

Its signature is static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll). The two wildcards do different jobs: Collection<? extends T> lets it accept any subtype collection (the collection produces Ts), and Comparable<? super T> lets a type be ordered by a comparator inherited from an ancestor.

class Animal implements Comparable<Animal> { /* ... */ }
class Dog extends Animal { }

List<Dog> dogs = List.of(new Dog(), new Dog());
Dog biggest = Collections.max(dogs); // Dog uses Animal's Comparable<Animal>

The super on Comparable is the key flexibility: Dog doesn't need its own compareTo — inheriting Comparable<Animal> suffices. This is PECS applied to the comparison itself.

boolean addAll(Collection<? extends E> c) uses an upper-bounded wildcard because the source collection is a produceraddAll only reads from c and writes into this. That lets you add a collection of any subtype of E.

List<Number> nums = new ArrayList<>();
List<Integer> ints = List.of(1, 2, 3);
nums.addAll(ints);     // ok — Integer extends Number (producer)

Without ? extends E you could only pass an exact Collection<E>, forcing callers to convert. This is the "Producer Extends" half of PECS in the standard library.

No. You can't write new with a wildcard type — the compiler needs a concrete type argument to know what to construct. Wildcards are for variable, parameter, and field types, not instantiation.

List<?> ok = new ArrayList<String>();   // wildcard on the VARIABLE — fine
List<?> bad = new ArrayList<?>();        // compile error — can't instantiate <?>
var list = new ArrayList<>();            // diamond infers <Object>, not a wildcard

The diamond <> is not a wildcard — it tells the compiler to infer a concrete type from context. A genuine <?> has no concrete type to allocate, so construction is forbidden.

A wildcard in a return type leaks into the caller's code: they're forced to deal with <?> (read-only, capture errors) even though it gave them no flexibility. The advice (Effective Java) is to use wildcards on parameters, not returns.

// bad: caller now holds a List<?> they can barely use
static List<?> bad() { return new ArrayList<String>(); }

// good: return a concrete or named type
static <T> List<T> good(T seed) { return new ArrayList<>(); }

If a method's return type would need a wildcard, that's usually a sign it should take a type parameter instead, so the caller gets a usable concrete type.

Wildcards apply at each level independently. List<List<?>> is a list whose elements are each "a list of some unknown type" — and crucially that outer list is invariant: it is not the same as List<List<String>>.

List<List<?>> outer = new ArrayList<>();
outer.add(List.of(1, 2));        // ok — inner is List<? = Integer>
outer.add(List.of("a"));         // ok — inner is List<? = String>

List<List<String>> strs = new ArrayList<>();
List<List<?>> alias = strs;      // compile error — outer level is invariant

To accept "a list of lists of anything" you need List<? extends List<?>> on the outer level too. Mixing levels (List<? extends List<? extends Number>>) is legal but a readability red flag.

A wildcard has no name, so you can't mention it in a return type — the best you could return is Object or another wildcard. A type parameter is named, so it can appear in the return position and flow that exact type back to the caller.

// wildcard: can't name the element type to return it
static Number first(List<? extends Number> l) { return l.get(0); } // only Number

// type parameter: returns the caller's exact T
static <T extends Number> T firstTyped(List<T> l) { return l.get(0); }
Integer i = firstTyped(List.of(1, 2)); // gets Integer back, not just Number

So when the output type must match the input element type, a named bounded type parameter is required.

A Comparator<? super T> lets you sort a List<T> with a comparator defined for T or any of its supertypes — the comparator is a consumer of Ts, so PECS says super. You shouldn't be forced to write a T-specific comparator when an ancestor's already works.

// List.sort signature: void sort(Comparator<? super E> c)
List<Dog> dogs = new ArrayList<>(/* ... */);
Comparator<Animal> byAge = Comparator.comparingInt(Animal::age);
dogs.sort(byAge);     // ok — a Comparator<Animal> can compare Dogs (super)

Without super you'd have to duplicate byAge as a Comparator<Dog>. This "Consumer Super" usage is one of the most common real-world wildcard payoffs.

The get-and-put principle is the same rule as PECS stated mechanically: use extends when you only get values out, use super when you only put values in, and use an exact type (no wildcard) when you need to do both.

List<? extends Number> producer = List.of(1, 2);
Number n = producer.get(0);   // get: ok    | put: forbidden

List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(7);              // put: ok     | get returns only Object

List<Integer> both = new ArrayList<>();
both.add(7); int x = both.get(0); // exact type: get AND put both work

So if a method must read and write the same element type, drop the wildcard and use a concrete type or a named type parameter.

More ways to practice

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

or
Join our WhatsApp Channel