Java Lower-Bounded Wildcards (? super)
Java Lower-Bounded Wildcards (? super)
A lower-bounded wildcard ? super T means "some unknown type that is T or a supertype of T." Where ? extends T (the upper-bounded wildcard) says "some subtype of T — you can read from it," ? super T says "some supertype of T — you can write T values into it." The two wildcards are mirror images: one unlocks reading across a family of subtypes, the other unlocks writing across a family of supertypes. ? super Integer accepts List<Integer>, List<Number>, and List<Object> — and any of those lists can safely receive an Integer added to it, because an Integer is also a Number and also an Object.
What Is a Lower-Bounded Wildcard?
A lower-bounded wildcard ? super T is a type argument representing an unknown type that is T or any supertype of T. The lower bound T establishes the floor — nothing below T in the hierarchy, and anything at or above it is allowed.
SYNTAX:
List<? super Integer> "a list of some type that is Integer or a supertype"
Consumer<? super String> "a consumer that accepts String or supertypes of String"
Comparator<? super Number> "a comparator that can compare Number or its supertypes"
WHAT TYPES SATISFY ? super Integer:
List<Integer> -> yes (Integer itself - the lower bound)
List<Number> -> yes (Number is a supertype of Integer)
List<Object> -> yes (Object is a supertype of Integer)
List<Double> -> NO (Double is not a supertype of Integer)
List<String> -> NO (String is not related to Integer)
List<Long> -> NO (Long is a sibling, not a supertype)
WHAT YOU CAN DO WITH List<? super Integer>:
Write -> list.add(42); // safe - Integer IS-A T, and T is a supertype of Integer
list.add(new Integer(7)); // safe for any list that accepts Integer or more general
Read -> Object element = list.get(0); // comes out as Object - cannot be typed further
Basic Overview - The Four Things to Know About Lower-Bounded Wildcards
1. THE CORE TRADE-OFF: WRITE ACCESS TO ANY SUPERTYPE CONTAINER
Fresher view : ? super Integer means you can ADD integers to a
List<Integer>, a List<Number>, or a List<Object>.
The same method works for all three - no duplication.
Deeper view : if a list holds Integer or any type MORE GENERAL than
Integer, then adding an Integer to it is always safe.
An Integer fits into a List<Number> (because Integer
is a Number). An Integer fits into a List<Object>
(because Integer is an Object). The compiler allows
add() because any supertype container CAN hold Integer.
2. WHY READING GIVES YOU ONLY Object
Fresher view : when you read from List<? super Integer>, you get
back Object - not Integer, not Number. The actual
list might be List<Number> or List<Object>, so the
only type guaranteed to represent ANY element is Object.
Deeper view : if the list is List<Number>, a get() might return a
Double (also a Number). If the list is List<Object>,
a get() might return a String. The wildcard type is
unknown, so the only safe return type is Object.
Reading from ? super lists is possible but coarse-
grained: you always get Object and must cast if you
need something more specific.
3. PECS - CONSUMER SUPER
Fresher view : ? super T is for lists your method WRITES INTO.
The list CONSUMES the values your method produces.
"Consumer Super" - this is the second half of PECS.
Deeper view : the full PECS rule (Producer Extends, Consumer Super)
together: use ? extends T when a collection PRODUCES
values for your code (you read), and ? super T when
a collection CONSUMES values from your code (you write).
Collections.copy(List<? super T> dest, List<? extends T> src)
is the canonical JDK example - src is the producer
(? extends), dest is the consumer (? super).
4. PECS APPLIED TO FUNCTION/CONSUMER TYPES
Fresher view : java.util.function.Consumer<? super T> means "a
consumer that can handle T or more general types."
If you have a Consumer<Object> and need a
Consumer<Integer>, Consumer<? super Integer>
accepts both.
Deeper view : functional interfaces follow PECS naturally.
A Comparator<? super T> can compare T values using
any comparison logic defined for T or its supertypes.
A Consumer<? super T> can accept T values because
anything that consumes Object can also consume T
(T IS-A supertype). This is contravariance in
functional types.
The Problem Lower-Bounded Wildcards Solve
The invariance of generic types creates a symmetric problem for writing that ? extends solves for reading. Just as List<Integer> cannot be passed as List<Number> for reading, a method that writes to a List<Number> cannot receive a List<Object> — even though an Object list could safely hold integers.
1// File: WhyLowerBoundedDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class WhyLowerBoundedDemo {
7
8 // ATTEMPT 1: write to List<Integer> only
9 // Too narrow - callers with List<Number> or List<Object> cannot use this
10 static void fillWithTenV1(List<Integer> dest) {
11 for (int i = 0; i < 5; i++) {
12 dest.add(10);
13 }
14 }
15
16 // ATTEMPT 2: write to List<Number>
17 // Better but still rejects List<Object> - and requires List<Number> specifically
18 static void fillWithTenV2(List<Number> dest) {
19 for (int i = 0; i < 5; i++) {
20 dest.add(10);
21 }
22 }
23
24 // CORRECT: ? super Integer accepts List<Integer>, List<Number>, List<Object>
25 // Adding an Integer to any of these is always type-safe
26 static void fillWithTen(List<? super Integer> dest) {
27 for (int i = 0; i < 5; i++) {
28 dest.add(10); // Integer - safe for any supertype list
29 }
30 }
31
32 public static void main(String[] args) {
33
34 List<Integer> intList = new ArrayList<>();
35 List<Number> numList = new ArrayList<>();
36 List<Object> objList = new ArrayList<>();
37
38 System.out.println("=== fillWithTen(List<? super Integer>) accepts all three ===");
39 fillWithTen(intList);
40 fillWithTen(numList);
41 fillWithTen(objList);
42 System.out.println("intList : " + intList);
43 System.out.println("numList : " + numList);
44 System.out.println("objList : " + objList);
45
46 System.out.println();
47
48 System.out.println("=== List<Integer> approach would reject List<Number> ===");
49 // fillWithTenV1(numList); // COMPILE ERROR
50 // "incompatible types: List<Number> cannot be converted to List<Integer>"
51
52 System.out.println("=== List<Number> approach would reject List<Object> ===");
53 // fillWithTenV2(objList); // COMPILE ERROR
54 // "incompatible types: List<Object> cannot be converted to List<Number>"
55 System.out.println("Both narrower approaches compile-error for wider containers");
56
57 System.out.println();
58
59 System.out.println("=== Reading from List<? super Integer> gives Object ===");
60 List<? super Integer> readable = numList;
61 Object element = readable.get(0); // Object - not Number, not Integer
62 System.out.println("Read as Object: " + element);
63 // Integer typed = readable.get(0); // COMPILE ERROR - cannot assign Object to Integer
64 System.out.println("Cannot read as Integer - actual list type is unknown");
65 }
66}Output:
=== fillWithTen(List<? super Integer>) accepts all three ===
intList : [10, 10, 10, 10, 10]
numList : [10, 10, 10, 10, 10]
objList : [10, 10, 10, 10, 10]
=== List<Integer> approach would reject List<Number> ===
=== List<Number> approach would reject List<Object> ===
Both narrower approaches compile-error for wider containers
=== Reading from List<? super Integer> gives Object ===
Read as Object: 10
Cannot read as Integer - actual list type is unknown
Writing to Lower-Bounded Wildcards
Writing works because the lower bound guarantees every supertype of T can accept T values — that is exactly what the IS-A relationship means. If the list holds Number, then adding an Integer (which IS-A Number) is safe. If the list holds Object, adding an Integer (which IS-A Object) is safe. The compiler verifies this through the lower bound.
1// File: LowerBoundWritingDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class LowerBoundWritingDemo {
7
8 // Copies all integers from source into dest.
9 // dest can be List<Integer>, List<Number>, or List<Object>.
10 // src is the producer (? extends) - reads integers from it.
11 // dest is the consumer (? super) - writes integers into it.
12 static void copyIntegers(List<? super Integer> dest, List<? extends Integer> src) {
13 for (Integer value : src) {
14 dest.add(value); // Integer into ? super Integer - always safe
15 }
16 }
17
18 // Populates a list with a sequence of integers from start to end (inclusive).
19 static void populateRange(List<? super Integer> dest, int start, int end) {
20 for (int i = start; i <= end; i++) {
21 dest.add(i); // adding Integer to any supertype container
22 }
23 }
24
25 // Writes a validation result (Boolean) into any list that can hold Boolean or supertypes
26 static void recordResult(List<? super Boolean> results, boolean passed) {
27 results.add(passed); // Boolean IS-A Object - always safe
28 }
29
30 public static void main(String[] args) {
31
32 System.out.println("=== copyIntegers - producer ? extends + consumer ? super ===");
33 List<Integer> source = List.of(100, 200, 300, 400, 500);
34
35 List<Number> numDest = new ArrayList<>();
36 List<Object> objDest = new ArrayList<>();
37
38 copyIntegers(numDest, source);
39 copyIntegers(objDest, source);
40
41 System.out.println("Into List<Number>: " + numDest);
42 System.out.println("Into List<Object>: " + objDest);
43
44 System.out.println();
45
46 System.out.println("=== populateRange into different supertype containers ===");
47 List<Integer> intTarget = new ArrayList<>();
48 List<Number> numTarget = new ArrayList<>();
49
50 populateRange(intTarget, 1, 5);
51 populateRange(numTarget, 10, 14);
52
53 System.out.println("List<Integer>: " + intTarget);
54 System.out.println("List<Number> : " + numTarget);
55
56 System.out.println();
57
58 System.out.println("=== recordResult with Boolean -> Object container ===");
59 List<Object> auditLog = new ArrayList<>();
60 recordResult(auditLog, true);
61 recordResult(auditLog, false);
62 recordResult(auditLog, true);
63 System.out.println("Audit log: " + auditLog);
64 }
65}Output:
=== copyIntegers - producer ? extends + consumer ? super ===
Into List<Number>: [100, 200, 300, 400, 500]
Into List<Object>: [100, 200, 300, 400, 500]
=== populateRange into different supertype containers ===
List<Integer>: [1, 2, 3, 4, 5]
List<Number> : [10, 11, 12, 13, 14]
=== recordResult with Boolean -> Object container ===
Audit log: [true, false, true]
The Complete PECS Rule
PECS — Producer Extends, Consumer Super — is the design rule that governs both wildcards together. Understanding both halves and how they interact is what makes generic API design deliberate rather than guesswork.
THE FULL PECS PICTURE:
static <T> void copy(List<? super T> dest, List<? extends T> src)
^^^^^^^^^^^ ^^^^^^^^^^^^^^
CONSUMER (writes T) PRODUCER (reads T)
? super T ? extends T
This is Collections.copy() - the canonical example in the JDK.
src PRODUCES T values -> method reads from src -> ? extends T
dest CONSUMES T values -> method writes to dest -> ? super T
WHY THE ASYMMETRY EXISTS:
Reading (? extends T):
List<? extends Number> can hold List<Integer>, List<Double>, etc.
Reading gives you Number (the upper bound).
Adding is forbidden - actual subtype unknown.
Writing (? super T):
List<? super Integer> can hold List<Integer>, List<Number>, List<Object>
Adding Integer (or subtypes of Integer) is always safe.
Reading gives you Object (the only guaranteed common supertype).
THE DECISION FLOW:
Parameter collects values YOUR CODE reads? -> ? extends T (PRODUCER)
Parameter receives values YOUR CODE writes? -> ? super T (CONSUMER)
Parameter does both? -> use named <T>, not wildcards
Parameter only structural (size/isEmpty)? -> plain ?
REAL JDK EXAMPLES:
Collections.copy(List<? super T> dest, List<? extends T> src)
Collections.sort(List<T> list, Comparator<? super T> c)
TreeSet(Comparator<? super E> comparator)
Stream.sorted(Comparator<? super T> comparator)
Optional.ifPresent(Consumer<? super T> consumer)
Stream.forEach(Consumer<? super T> action)
1// File: FullPecsDemo.java
2
3import java.util.*;
4import java.util.function.*;
5
6public class FullPecsDemo {
7
8 // Demonstrates both halves of PECS in a single meaningful method.
9 // src PRODUCES T (read with ? extends) - the source of truth
10 // dest CONSUMES T (write with ? super) - the destination
11 // transform is a function from T to R - output type R
12 static <T, R> void transformAndCollect(
13 List<? extends T> src,
14 Function<T, R> transform,
15 List<? super R> dest) {
16 for (T item : src) {
17 dest.add(transform.apply(item));
18 }
19 }
20
21 // Demonstrates ? super with Comparator - the Consumer side of comparison
22 static <T> void sortDescending(List<T> items, Comparator<? super T> comparator) {
23 items.sort(comparator.reversed());
24 }
25
26 // Demonstrates ? super with java.util.function.Consumer
27 static <T> void processAll(Iterable<? extends T> source, Consumer<? super T> action) {
28 for (T item : source) {
29 action.accept(item);
30 }
31 }
32
33 public static void main(String[] args) {
34
35 System.out.println("=== transformAndCollect - PECS both sides ===");
36 List<Integer> prices = List.of(499, 1299, 799, 2499);
37 List<Number> asNumbers = new ArrayList<>(); // dest is ? super Double (Number qualifies)
38 List<Object> formatted = new ArrayList<>();
39
40 // T=Integer, R=Double, dest=List<Number> satisfies ? super Double
41 transformAndCollect(prices, price -> price * 1.18, asNumbers);
42 System.out.println("Prices with GST (into List<Number>): " + asNumbers);
43
44 // T=Integer, R=String, dest=List<Object> satisfies ? super String
45 transformAndCollect(prices, price -> "Rs." + price, formatted);
46 System.out.println("Formatted prices (into List<Object>): " + formatted);
47
48 System.out.println();
49
50 System.out.println("=== sortDescending - Comparator<? super T> ===");
51 List<Integer> scores = new ArrayList<>(List.of(72, 95, 88, 64, 91));
52 // Comparator<Object> satisfies Comparator<? super Integer>
53 Comparator<Object> byStringLength = Comparator.comparing(Object::toString);
54 sortDescending(scores, Comparator.naturalOrder()); // Comparator<Integer>
55 System.out.println("Scores descending: " + scores);
56
57 System.out.println();
58
59 System.out.println("=== processAll - Consumer<? super T> ===");
60 List<Integer> orderIds = List.of(5001, 5002, 5003);
61
62 // Consumer<Object> satisfies Consumer<? super Integer>
63 Consumer<Object> printer = item -> System.out.println(" Processing: " + item);
64 processAll(orderIds, printer);
65 }
66}Output:
=== transformAndCollect - PECS both sides ===
Prices with GST (into List<Number>): [589.02, 1532.82, 943.02, 2949.02]
Formatted prices (into List<Object>): [Rs.499, Rs.1299, Rs.799, Rs.2499]
=== sortDescending - Comparator<? super T> ===
Scores descending: [95, 91, 88, 72, 64]
=== processAll - Consumer<? super T> ===
Processing: 5001
Processing: 5002
Processing: 5003
Lower-Bounded Wildcards With Functional Interfaces
The java.util.function package uses ? super T throughout because functional interfaces are consumers from the perspective of the type they receive. Understanding this makes the signatures of Stream.forEach, Optional.ifPresent, Comparator.comparing, and similar methods much less mysterious.
1// File: FunctionalWildcardDemo.java
2
3import java.util.*;
4import java.util.function.*;
5import java.util.stream.*;
6
7public class FunctionalWildcardDemo {
8
9 record Product(String name, String category, double price) {}
10
11 // Custom method that mirrors Stream.forEach signature using Consumer<? super T>
12 static <T> void forEach(List<? extends T> items, Consumer<? super T> action) {
13 for (T item : items) {
14 action.accept(item); // action consumes T (or supertypes) - ? super T
15 }
16 }
17
18 // Returns the first match, applying a Predicate<? super T>
19 // Predicate is a consumer of values for testing - ? super makes it accept any supertype pred
20 static <T> Optional<T> findFirst(List<? extends T> items, Predicate<? super T> predicate) {
21 for (T item : items) {
22 if (predicate.test(item)) return Optional.of(item);
23 }
24 return Optional.empty();
25 }
26
27 public static void main(String[] args) {
28
29 List<Product> products = List.of(
30 new Product("Wireless Mouse", "Electronics", 799.0),
31 new Product("Laptop Stand", "Accessories", 1299.0),
32 new Product("USB Hub", "Electronics", 499.0),
33 new Product("Keyboard", "Electronics", 2499.0)
34 );
35
36 System.out.println("=== forEach with Consumer<Object> (? super Product satisfied) ===");
37 Consumer<Object> printer = item -> System.out.println(" Item: " + item);
38 forEach(products, printer); // Consumer<Object> satisfies Consumer<? super Product>
39
40 System.out.println();
41
42 System.out.println("=== findFirst with Predicate<Object> ===");
43 Predicate<Object> nonNull = obj -> obj != null;
44 Optional<Product> anyProduct = findFirst(products, nonNull);
45 System.out.println("First non-null: " + anyProduct.map(Product::name).orElse("none"));
46
47 System.out.println();
48
49 System.out.println("=== Stream.sorted with Comparator<? super T> in practice ===");
50 // Comparator<Object> comparing by toString() - satisfies Comparator<? super Product>
51 Comparator<Object> byString = Comparator.comparing(Object::toString);
52
53 // Comparator<Product> - the specific type
54 Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
55
56 List<Product> sortedByPrice = products.stream()
57 .sorted(byPrice) // Comparator<Product> satisfies Comparator<? super Product>
58 .collect(Collectors.toList());
59
60 System.out.println("Sorted by price:");
61 sortedByPrice.forEach(p ->
62 System.out.printf(" %-20s Rs.%.2f%n", p.name(), p.price()));
63 }
64}Output:
=== forEach with Consumer<Object> (? super Product satisfied) ===
Item: Product[name=Wireless Mouse, category=Electronics, price=799.0]
Item: Product[name=Laptop Stand, category=Accessories, price=1299.0]
Item: Product[name=USB Hub, category=Electronics, price=499.0]
Item: Product[name=Keyboard, category=Electronics, price=2499.0]
=== findFirst with Predicate<Object> ===
First non-null: Wireless Mouse
=== Stream.sorted with Comparator<? super T> in practice ===
Sorted by price:
USB Hub Rs.499.00
Wireless Mouse Rs.799.00
Laptop Stand Rs.1299.00
Keyboard Rs.2499.00
Real-World Example - Swiggy Event Publishing System
A food-delivery platform's event system publishes different event types — OrderPlacedEvent, OrderDeliveredEvent, PaymentFailedEvent — to a collection of listeners. Each listener may be registered with a broad type (listening for any Event) or a narrow type (listening only for OrderPlacedEvent). The publisher uses ? super T to write events to listeners that can handle T or any supertype of T, enabling a single publish method to route to any compatible listener.
1// File: Event.java
2
3public abstract class Event {
4 private final String eventId;
5 private final long timestamp;
6
7 protected Event(String eventId) {
8 this.eventId = eventId;
9 this.timestamp = System.currentTimeMillis();
10 }
11
12 public String getEventId() { return eventId; }
13 public long getTimestamp() { return timestamp; }
14 public abstract String getType();
15}1// File: OrderEvent.java
2
3public class OrderEvent extends Event {
4 private final String orderId;
5 private final String customerId;
6
7 public OrderEvent(String eventId, String orderId, String customerId) {
8 super(eventId);
9 this.orderId = orderId;
10 this.customerId = customerId;
11 }
12
13 public String getOrderId() { return orderId; }
14 public String getCustomerId() { return customerId; }
15
16 @Override
17 public String getType() { return "ORDER_EVENT"; }
18
19 @Override
20 public String toString() {
21 return "OrderEvent[" + getEventId() + ", orderId=" + orderId + "]";
22 }
23}1// File: OrderPlacedEvent.java
2
3public class OrderPlacedEvent extends OrderEvent {
4 private final double orderAmount;
5
6 public OrderPlacedEvent(String orderId, String customerId, double orderAmount) {
7 super("EVT-PLACED-" + orderId, orderId, customerId);
8 this.orderAmount = orderAmount;
9 }
10
11 public double getOrderAmount() { return orderAmount; }
12
13 @Override
14 public String getType() { return "ORDER_PLACED"; }
15
16 @Override
17 public String toString() {
18 return "OrderPlacedEvent[orderId=" + getOrderId()
19 + ", amount=Rs." + orderAmount + "]";
20 }
21}1// File: EventHandler.java
2
3@FunctionalInterface
4public interface EventHandler<T extends Event> {
5 void handle(T event);
6}1// File: EventPublisher.java
2
3import java.util.*;
4
5public class EventPublisher {
6
7 // Handler map: event type class -> list of handlers for that type.
8 // Each handler list uses ? super T so that handlers registered for
9 // supertype events (e.g., Event or OrderEvent) also receive
10 // OrderPlacedEvent, because OrderPlacedEvent IS-A OrderEvent IS-A Event.
11 private final Map<Class<?>, List<EventHandler<? super Event>>> handlers = new HashMap<>();
12
13 // Register a handler for a specific event type.
14 // The handler accepts E or any supertype of E - hence ? super E.
15 @SuppressWarnings("unchecked")
16 public <E extends Event> void register(Class<E> eventType,
17 EventHandler<? super E> handler) {
18 handlers.computeIfAbsent(eventType, k -> new ArrayList<>())
19 .add((EventHandler<? super Event>) handler);
20 }
21
22 // Publish an event to all registered handlers for its type and supertypes.
23 // Uses ? super T when calling handlers - the handler can consume the event
24 // if it is registered for T or any supertype of T.
25 @SuppressWarnings("unchecked")
26 public <E extends Event> void publish(E event) {
27 System.out.println("[PUBLISHER] Publishing: " + event);
28
29 Class<?> eventClass = event.getClass();
30 while (eventClass != null && Event.class.isAssignableFrom(eventClass)) {
31 List<EventHandler<? super Event>> matchedHandlers =
32 handlers.getOrDefault(eventClass, List.of());
33
34 for (EventHandler<? super Event> handler : matchedHandlers) {
35 handler.handle(event); // event IS-A Event - safe to pass
36 }
37
38 eventClass = eventClass.getSuperclass();
39 }
40 }
41}1// File: SwiggyEventDemo.java
2
3public class SwiggyEventDemo {
4
5 public static void main(String[] args) {
6 EventPublisher publisher = new EventPublisher();
7
8 // Handler registered for Event (the most general type).
9 // Any event reaches this handler via the ? super mechanism.
10 publisher.register(Event.class, event ->
11 System.out.println(" [AUDIT] Event received: "
12 + event.getType() + " / " + event.getEventId()));
13
14 // Handler registered for OrderEvent.
15 // OrderPlacedEvent (a subtype) also reaches this handler.
16 publisher.register(OrderEvent.class, orderEvent ->
17 System.out.println(" [ORDER] Order: " + orderEvent.getOrderId()
18 + " for customer: " + orderEvent.getCustomerId()));
19
20 // Handler registered specifically for OrderPlacedEvent.
21 // Only OrderPlacedEvent (and subtypes) reach this handler.
22 publisher.register(OrderPlacedEvent.class, placedEvent ->
23 System.out.printf(" [PAYMENT] Amount Rs.%.2f ready for processing%n",
24 placedEvent.getOrderAmount()));
25
26 System.out.println("=== Publishing OrderPlacedEvent ===");
27 OrderPlacedEvent placed = new OrderPlacedEvent("ORD-8801", "CUST-4412", 1499.0);
28 publisher.publish(placed);
29
30 System.out.println();
31
32 System.out.println("=== Publishing a base OrderEvent ===");
33 OrderEvent orderOnly = new OrderEvent("EVT-ORD-8802", "ORD-8802", "CUST-4413") {
34 @Override public String getType() { return "ORDER_CANCELLED"; }
35 };
36 publisher.publish(orderOnly);
37 }
38}Output:
=== Publishing OrderPlacedEvent ===
[PUBLISHER] Publishing: OrderPlacedEvent[orderId=ORD-8801, amount=Rs.1499.0]
[PAYMENT] Amount Rs.1499.00 ready for processing
[ORDER] Order: ORD-8801 for customer: CUST-4412
[AUDIT] Event received: ORDER_PLACED / EVT-PLACED-ORD-8801
=== Publishing a base OrderEvent ===
[PUBLISHER] Publishing: OrderEvent[EVT-ORD-8802, orderId=ORD-8802]
[ORDER] Order: ORD-8802 for customer: CUST-4413
[AUDIT] Event received: ORDER_CANCELLED / EVT-ORD-8802
The event hierarchy is OrderPlacedEvent extends OrderEvent extends Event. When OrderPlacedEvent is published, all three handlers fire: the most specific (OrderPlacedEvent handler) first, then the intermediate (OrderEvent handler), then the broadest (Event handler). Each handler is registered with EventHandler<? super E> — the general Event handler is EventHandler<Event>, which satisfies EventHandler<? super OrderPlacedEvent> because Event is a supertype of OrderPlacedEvent. This is the PECS consumer side in a real observer pattern: each handler is a consumer of events, consuming from the most specific type upward.
Comparing All Three Wildcard Forms
| Wildcard | Accepts | Read type | Can write | Typical use |
|---|---|---|---|---|
List<?> | Any List<X> | Object | Only null | Read-only structural operations (size, print) |
List<? extends T> | Any List<X> where X is T or subtype | T | Only null | Reading from any subtype collection (PECS producer) |
List<? super T> | Any List<X> where X is T or supertype | Object | Any T or subtype | Writing into any supertype collection (PECS consumer) |
Best Practices
Apply ? super T to every method parameter that your code writes into. The PECS rule is mechanically applicable: for every collection parameter, if your method calls add() or set(), that parameter needs ? super T. This widens the set of lists callers can pass, making utility methods genuinely reusable rather than requiring callers to construct the exact parameterized type.
Use ? super T for Comparator, Consumer, and Predicate parameters. When a method accepts a Comparator to sort with, declaring it as Comparator<? super T> means callers can pass a Comparator<Object> (which compares everything) in addition to Comparator<T> (which compares the specific type). This is why Collections.sort(List<T>, Comparator<? super T>) is more flexible than Collections.sort(List<T>, Comparator<T>) — the former accepts any general-purpose comparator the caller already has.
Do not use ? super T when reading is the only operation. If a method only reads from a collection, ? super T is the wrong choice — it restricts what types can be read back (only Object) while unnecessarily complicating the signature. Use ? extends T for reading and ? super T for writing.
Avoid combining both wildcards on the same parameter. A single collection parameter cannot serve as both a producer and a consumer through wildcards. If a method needs to both read and write to the same collection, use a named type parameter <T> rather than any wildcard — <T> void process(List<T> items) allows both get() and add().
Common Mistakes
Mistake 1 - Trying to Read a Specific Type From List<? super T>
1import java.util.List;
2import java.util.ArrayList;
3
4// WRONG - reading from List<? super Integer> gives Object, not Integer.
5// The actual list might be List<Number> or List<Object>, containing
6// non-Integer elements. Casting is unsafe.
7static void tryToReadTyped(List<? super Integer> items) {
8 // Integer first = items.get(0); // COMPILE ERROR - get() returns Object
9 Object raw = items.get(0); // this compiles - but gives Object only
10
11 // Casting is possible but risky - no compile-time guarantee
12 // Integer value = (Integer) items.get(0); // ClassCastException if list holds Number or Double
13}
14
15// CORRECT - if typed reading is needed, use ? extends T instead
16static void readTyped(List<? extends Number> items) {
17 Number first = items.get(0); // Number - typed correctly for reading
18 System.out.println(first.doubleValue()); // Number methods available
19}Mistake 2 - Confusing Which Wildcard Enables Writing
1import java.util.List;
2import java.util.ArrayList;
3
4// WRONG - ? extends T prevents writing, but many developers mix this up.
5// This is the most common PECS mistake: using extends when super is needed.
6static void addToExtends(List<? extends Number> dest) {
7 // dest.add(42); // COMPILE ERROR - cannot add to ? extends
8 // dest.add(3.14); // COMPILE ERROR
9 // ? extends restricts writes to protect subtypes from corruption
10}
11
12// CORRECT - ? super T enables writing T values into the list
13static void addToSuper(List<? super Integer> dest) {
14 dest.add(42); // Integer into ? super Integer - safe
15 dest.add(100); // Integer into ? super Integer - safe
16 System.out.println("Added to dest: size=" + dest.size());
17}Mistake 3 - Using ? super When the Method Only Reads
1import java.util.List;
2
3// WRONG - ? super Integer is the consumer wildcard. But this method
4// only READS from the list (prints each element). Using ? super here:
5// 1. Forces reading as Object (coarser than necessary)
6// 2. Misleads reviewers - looks like the method writes to the list
7// 3. Is semantically incorrect for the PECS rule (reader = producer = extends)
8static void printIntegersBroken(List<? super Integer> items) {
9 for (Object item : items) { // Object - cannot call Integer methods
10 System.out.println(item);
11 }
12}
13
14// CORRECT for read-only: use ? extends to signal "this is a producer"
15// and to get Integer-typed elements
16static void printIntegers(List<? extends Integer> items) {
17 for (Integer item : items) { // Integer - typed correctly
18 System.out.println(item * 2); // Integer arithmetic available
19 }
20}Mistake 4 - Applying ? super to Both Parameters When PECS Requires Different Wildcards
1import java.util.List;
2
3// WRONG - both parameters use ? super T but one of them is a producer
4// (source is read from) and one is a consumer (dest is written to).
5// Using ? super on the source prevents reading elements as T - you
6// get Object instead, losing the type relationship.
7static <T> void copyWrong(List<? super T> dest, List<? super T> source) {
8 for (Object item : source) { // Object - not T - type lost for source
9 // dest.add(item); // COMPILE ERROR - cannot add Object to ? super T
10 }
11}
12
13// CORRECT - follow PECS:
14// source PRODUCES T values -> ? extends T (producer)
15// dest CONSUMES T values -> ? super T (consumer)
16static <T> void copyCorrect(List<? super T> dest, List<? extends T> source) {
17 for (T item : source) { // T - typed correctly for source
18 dest.add(item); // T into ? super T - always safe
19 }
20}Interview Questions
Q1. What does List<? super Integer> mean, and which types of lists can be passed to it?
List<? super Integer> means "a list of some type that is Integer or a supertype of Integer." It accepts List<Integer> (Integer itself, the lower bound), List<Number> (Number is a supertype of Integer), and List<Object> (Object is a supertype of Integer). It rejects List<Double> (Double is not a supertype of Integer — it is a sibling in the hierarchy) and List<String> (completely unrelated). Adding an Integer or any subtype of Integer to such a list is always type-safe, because every supertype of Integer can hold an Integer.
Q2. What type do you get back when reading from a List<? super Integer>?
Reading from List<? super Integer> gives Object — the only type the compiler can guarantee is common to all possible actual list types. The actual list might be List<Number> (containing Doubles as well as Integers), or List<Object> (containing anything). Since the element type is unknown and could be wider than Integer or Number, the compiler types every element as Object. If typed access to elements is needed, the parameter should use ? extends Integer instead. The rule of thumb: ? super T is for writing, not for reading typed values.
Q3. What is the PECS rule, and how does ? super fit into it?
PECS stands for Producer Extends, Consumer Super. A collection that your method reads from is a "producer" — it produces values for your algorithm — and should use ? extends T. A collection that your method writes into is a "consumer" — it consumes values your algorithm produces — and should use ? super T. ? super T is the consumer side of PECS: by accepting any supertype of T, it allows a method to write T values into List<Integer>, List<Number>, or List<Object> interchangeably. Collections.copy(List<? super T> dest, List<? extends T> src) is the canonical JDK example showing both sides simultaneously.
Q4. Why does Stream.sorted(Comparator<? super T>) use ? super instead of Comparator
Comparator<? super T> makes the sorted method work with any comparator that can compare T values — not just comparators specifically designed for T, but also comparators designed for any supertype of T. A Comparator<Object> that compares everything by toString() can sort a List<String> because String is a subtype of Object. With Comparator<T>, a general-purpose Comparator<Object> would be rejected even though it can compare strings perfectly well. The ? super makes the API more composable: developers can pass general-purpose comparators they already have rather than creating type-specific ones.
Q5. How does ? super work with functional interfaces like Consumer<? super T>?
A Consumer<? super T> accepts any consumer that can handle T or supertypes of T. If a method declares Consumer<? super String> as a parameter, callers can pass a Consumer<String> (handles strings specifically), a Consumer<CharSequence> (handles any char sequence), or a Consumer<Object> (handles anything). This is PECS at the function type level: the consumer "consumes" values of type T, so ? super T allows more general consumers to be used safely. The consumer's accept(T value) call is always safe because T IS-A every supertype of T. This is why Stream.forEach(Consumer<? super T>), Optional.ifPresent(Consumer<? super T>), and similar streaming API methods use ? super T for their functional parameters.
Q6. When would you use a plain named type parameter
Use a named type parameter <T> instead of ? super T when: the method needs to both read and write to the same collection (wildcards cannot do both cleanly); the return type must reference the element type (wildcards have no name to return); the element type must appear in two or more places in the signature; or the method body needs to create instances of T or call type-specific methods through a typed variable. The wildcard ? super T is specifically for method parameters representing write-only collection destinations. The moment you need to read a typed value back from that same collection, or reference the element type anywhere else, switch to <T>.
FAQs
Can ? super T be used in a return type?
Not meaningfully. List<? super Integer> as a return type is syntactically legal but practically useless — the caller receives a List<? super Integer> reference, can add Integer values to it, but can only read Object elements back. The return type conveys almost no useful information and is rarely what a caller needs. Methods that return collections almost always use a named type parameter or a concrete parameterized type to give the caller full typed access.
What is the difference between ? super T and raw type?
A raw type — List instead of List<Integer> or List<? super Integer> — abandons all generic type information, accepting anything and returning Object, and produces unchecked compiler warnings throughout. List<? super Integer> retains the lower bound: the compiler knows the list can safely accept Integer values (and enforces this), while reads give Object. The raw type is backward-compatibility mode; ? super Integer is intentional, type-safe API design. Raw types should never appear in new code.
Does ? super T work the same way for arrays as for collections?
Java arrays have covariance built in (String[] IS-A Object[]), not the invariance that makes wildcards necessary for collections. You can pass a String[] where Object[] is expected directly, without any wildcard. Wildcards are a feature of generics only — arrays have their own (historically flawed) type compatibility rules. The downside of array covariance is that it allows ArrayStoreException at runtime; generics with wildcards prevent this by enforcing constraints at compile time.
Can you have a lower-bounded wildcard on a type parameter, like
No. Lower bounds (super) are only available on wildcard type arguments (? super Integer), not on named type parameter declarations (<T super Integer>). Type parameter bounds can only use extends — the upper bound. This asymmetry is a deliberate design decision in Java's type system. When you need a lower bound on a named type, restructure to use ? super T at the usage site and keep the type parameter itself unbounded or upper-bounded.
Is ? super T contravariant?
Yes. List<? super Integer> is contravariant with respect to Integer: as the element type goes UP the hierarchy (Integer -> Number -> Object), List<? super Integer> accepts MORE types (contravariant direction, opposite of covariant). List<? extends Number> is covariant: as the bound goes UP, fewer types are accepted. This mirrors the concept of contravariance in type theory and functional programming: consumer types are contravariant, producer types are covariant.
Can you use ? super T in a class field declaration, and when does it make sense?
Yes — List<? super Integer> values is a valid field type. It is useful when a class needs to store a reference to an injected collection that it will write into, without caring about the exact parameterization. For example, a class that accumulates audit entries into an externally provided list could declare private final List<? super AuditEntry> sink and accept any list that can hold audit entries or more general objects. Reading back from such a field gives Object, but for write-only accumulators that design is intentional and correct.
Summary
The lower-bounded wildcard ? super T is the write-enabling half of Java's wildcard system. It accepts any collection whose element type is T or a supertype of T, and it allows adding T values (and subtypes of T) to that collection safely. The trade-off is symmetric to ? extends T: gaining the ability to write, you give up typed reading — elements come out as Object.
PECS — Producer Extends, Consumer Super — is the complete rule: if a collection parameter produces values your code reads, declare it with ? extends T; if it consumes values your code writes, declare it with ? super T. Collections.copy(List<? super T> dest, List<? extends T> src) is the canonical example of both sides in one signature, and Stream.sorted(Comparator<? super T>), Stream.forEach(Consumer<? super T>), and Optional.ifPresent(Consumer<? super T>) are the most common functional interface applications of the consumer side.
The practical test before choosing a wildcard: ask what the collection does relative to the method. If the method reads from it, it is a producer — use ? extends. If the method writes to it, it is a consumer — use ? super. If it does both, use a named type parameter. This one decision, applied consistently, produces correct wildcard choices across any generic API.