What is PECS (Producer Extends Consumer Super)
Structured Java interview answer with junior, middle, and senior-level explanation.
π’ Junior Level
PECS is a mnemonic rule for working with wildcards in Java generics:
- Producer Extends β if you need to read data, use
? extends T - Consumer Super β if you need to write data, use
? super T
// Producer Extends β read only
List<? extends Number> numbers = List.of(1, 2, 3);
Number n = numbers.get(0); // β
can read
// numbers.add(4); // β cannot write
// Consumer Super β write only
List<? super Integer> integers = new ArrayList<>();
integers.add(42); // β
can write
// Integer i = integers.get(0); // β cannot read (only Object)
Simple analogy:
- Producer (supplier) β gives data β
extends - Consumer (consumer) β accepts data β
super
π‘ Middle Level
How it works
Producer Extends:
// Reading from source β this is a producer
public static double sum(List<? extends Number> source) {
return source.stream()
.mapToDouble(Number::doubleValue) // β
can read Number
.sum();
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
sum(ints); // β
Integer extends Number
sum(doubles); // β
Double extends Number
Consumer Super:
// Writing to target β this is a consumer
public static void addNumbers(List<? super Integer> target, int count) {
for (int i = 0; i < count; i++) {
target.add(i); // β
can write Integer
}
}
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(ints, 3); // β
Integer super Integer
addNumbers(numbers, 3); // β
Number super Integer
addNumbers(objects, 3); // β
Object super Integer
Full PECS rule
// Both producer AND consumer β no wildcard
public static <T> void copy(List<T> dest, List<T> src) {
dest.addAll(src); // both reading and writing
}
// Correct signature (as in JDK Collections.max):
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// ? super T allows compareTo to be inherited from a superclass.
// Consumer only β super
public static <T> void fill(List<? super T> list, T value, int count) {
for (int i = 0; i < count; i++) {
list.add(value);
}
}
Common mistakes
- Confusing direction: ```java // β Wrong List<? super Number> numbers = List.of(1, 2, 3); // error // numbers.get(0); // can only get Object
// β Correct β producer uses extends List<? extends Number> numbers = List.of(1, 2, 3); Number n = numbers.get(0); // β
2. **Both directions β no wildcard needed:**
```java
// β Useless
public void process(List<? extends T> list) {
list.add(item); // β cannot
}
// β
If you need both read and write β no wildcard
public <T> void process(List<T> list) {
list.add(list.get(0)); // β
everything works
}
π΄ Senior Level
Internal Implementation
Compile-time check:
List<? extends Number> extendsList = new ArrayList<Integer>();
List<? super Number> superList = new ArrayList<Object>();
// Extends β compiler knows lower bound
Number n1 = extendsList.get(0); // β
Number β minimum
// extendsList.add(42); // β type unknown
// Super β compiler knows upper bound
superList.add(42); // β
Number will definitely fit
Object o = superList.get(0); // β
Object β maximum
Capture conversion:
// Reverse for a list β both reading and writing
public static <T> void reverse(List<T> list) {
int size = list.size();
for (int i = 0; i < size / 2; i++) {
T temp = list.get(i);
list.set(i, list.get(size - 1 - i));
list.set(size - 1 - i, temp);
}
}
// Cannot use wildcard β need both read and write
// reverse(List<? extends T>) β cannot set
// reverse(List<? super T>) β cannot get specific type
Architectural Trade-offs
| Situation | Wildcard | Read | Write |
|---|---|---|---|
| Read only | ? extends T |
β T | β |
| Write only | ? super T |
Object | β T |
| Both | <T> |
β T | β T |
| Unknown type | ? |
Object | null only |
Edge Cases
1. Nested PECS:
// Comparator β consumer
Comparator<? super T> comparator
// Factory β producer
public static <T> List<T> produce(List<? extends T> source) {
return new ArrayList<>(source);
}
// Combination
public static <T> void sort(List<T> list, Comparator<? super T> comp) {
list.sort(comp); // list β both read/write, comp β consumer
}
2. Stream API:
// Collector<T, A, R> β A is accumulator (consumer)
public static <T> Collector<T, ?, List<T>> toList() {
return Collectors.toList();
}
// ? β accumulator type unknown (implementation detail)
3. Functional interfaces:
// Function β producer (returns R)
Function<? super T, ? extends R>
// Consumer β consumer (accepts T)
Consumer<? super T>
// Supplier β producer (returns T)
Supplier<? extends T>
Performance
PECS:
- Runtime: Zero overhead
- Compile time: strict type checking
- Safety: prevents ClassCastException
Performance is the same for all wildcard types
Production Experience
JDK examples:
// Collections.copy β classic PECS
public static <T> void copy(
List<? super T> dest, // consumer β writing T
List<? extends T> src // producer β reading T
) {
Iterator<? extends T> si = src.iterator();
for (int i = 0; i < src.size(); i++) {
dest.set(i, si.next());
}
}
// Stream.collect
public final class Stream<T> {
public <R, A> R collect(
Collector<? super T, A, R> collector // T β consumer
) { }
}
// Optional.orElseGet
public T orElseGet(Supplier<? extends T> supplier) { // T β producer
return value != null ? value : supplier.get();
}
Real-world cases:
// Repository pattern
public interface Repository<T> {
// Producer β returns data
Optional<? extends T> findById(Long id);
List<? extends T> findAll();
// Consumer β accepts data
void save(T entity);
void saveAll(List<? extends T> entities);
}
// Event Publisher
public class EventPublisher<T extends Event> {
private final List<Consumer<? super T>> listeners = new ArrayList<>();
public void subscribe(Consumer<? super T> listener) {
listeners.add(listener); // consumer
}
public void publish(T event) {
listeners.forEach(l -> l.accept(event));
}
}
Best Practices
// β
PECS for maximum flexibility
public static <T> void copy(List<? super T> dest, List<? extends T> src) { }
// β
Producer for reading
public double sum(List<? extends Number> numbers) { }
// β
Consumer for writing
public void fill(List<? super Integer> list, int value) { }
// β
Both β no wildcard
public <T> void swap(List<T> list, int i, int j) { }
// β Extends for writing
// β Super for reading a specific type
// β Wildcard when you need both read and write
π― Interview Cheat Sheet
Must know:
- PECS: Producer Extends β reading data, Consumer Super β writing data
List<? extends Number>β producer: read as Number, write forbiddenList<? super Integer>β consumer: write Integer, read only as Object- When you need both read and write β use
<T>, no wildcard - JDK examples:
Collections.copy(List<? super T>, List<? extends T>),Collections.max(Collection<? extends T>) - PECS β use-site variance, differs from Kotlin
out T/in T(declaration-site)
Frequent follow-up questions:
- Give a PECS example from JDK? β
Collections.copy(dest, src),Stream.collect(Collector) - When is wildcard NOT needed? β When the method both reads and writes (swap, reverse)
- What is nested PECS? β
Function<? super T, ? extends R>β T consumer, R producer - Can you add null to List<? extends T>? β Yes, null is valid for any type
Red flags (DO NOT say):
- β βPECS prevents ClassCastException at runtimeβ β Itβs a compile-time check
- β βExtends allows writing subtypesβ β Extends forbids writing entirely
- β βSuper allows reading as Tβ β Super allows reading only as Object
- β βPECS is a design patternβ β Itβs a mnemonic rule for wildcards
Related topics:
- [[11. What are Generics in Java]]
- [[13. What is type erasure]]
- [[16. What is the difference between <? extends T> and <? super T>]]
- [[19. What are raw types and why should you avoid them]]