Що таке PECS (Producer Extends Consumer Super)
Structured Java interview answer with junior, middle, and senior-level explanation.
🟢 Junior Level
PECS — це мнемонічне правило для роботи з wildcard в Java дженериках:
- Producer Extends — якщо потрібно читати дані, використовуй
? extends T - Consumer Super — якщо потрібно записувати дані, використовуй
? super T
// Producer Extends — тільки читаємо
List<? extends Number> numbers = List.of(1, 2, 3);
Number n = numbers.get(0); // ✅ можна читати
// numbers.add(4); // ❌ не можна писати
// Consumer Super — тільки записуємо
List<? super Integer> integers = new ArrayList<>();
integers.add(42); // ✅ можна писати
// Integer i = integers.get(0); // ❌ не можна читати (тільки Object)
Проста аналогія:
- Producer (постачальник) — віддає дані →
extends - Consumer (споживач) — приймає дані →
super
🟡 Middle Level
Як це працює
Producer Extends:
// Читаємо з source — це producer
public static double sum(List<? extends Number> source) {
return source.stream()
.mapToDouble(Number::doubleValue) // ✅ можна читати 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:
// Записуємо в target — це consumer
public static void addNumbers(List<? super Integer> target, int count) {
for (int i = 0; i < count; i++) {
target.add(i); // ✅ можна писати 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
Повне правило PECS
// І producer І consumer — без wildcard
public static <T> void copy(List<T> dest, List<T> src) {
dest.addAll(src); // і читаємо, і пишемо
}
// Правильна сигнатура (як в JDK Collections.max):
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// ? super T дозволяє compareTo бути успадкованим від суперкласу.
// Тільки consumer — super
public static <T> void fill(List<? super T> list, T value, int count) {
for (int i = 0; i < count; i++) {
list.add(value);
}
}
Типові помилки
- Плутанина напрямку: ```java // ❌ Неправильно List<? super Number> numbers = List.of(1, 2, 3); // помилка // numbers.get(0); // можна тільки Object
// ✅ Правильно — producer використовує extends List<? extends Number> numbers = List.of(1, 2, 3); Number n = numbers.get(0); // ✅
2. **Обидва напрямки — wildcard не потрібен:**
```java
// ❌ Марно
public void process(List<? extends T> list) {
list.add(item); // ❌ не можна
}
// ✅ Якщо потрібно і читати, і писати — без wildcard
public <T> void process(List<T> list) {
list.add(list.get(0)); // ✅ можна все
}
🔴 Senior Level
Internal Implementation
Compile-time перевірка:
List<? extends Number> extendsList = new ArrayList<Integer>();
List<? super Number> superList = new ArrayList<Object>();
// Extends — компілятор знає lower bound
Number n1 = extendsList.get(0); // ✅ Number — мінімум
// extendsList.add(42); // ❌ тип невідомий
// Super — компілятор знає upper bound
superList.add(42); // ✅ Number точно підійде
Object o = superList.get(0); // ✅ Object — максимум
Capture conversion:
// Reverse для списку — і читаємо, і пишемо
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);
}
}
// Не можна використовувати wildcard — потрібно і read, і write
// reverse(List<? extends T>) — не можна set
// reverse(List<? super T>) — не можна get конкретного типу
Архітектурні Trade-offs
| Ситуація | Wildcard | Read | Write |
|---|---|---|---|
| Тільки читання | ? extends T |
✅ T | ❌ |
| Тільки запис | ? super T |
Object | ✅ T |
| І те, й інше | <T> |
✅ T | ✅ T |
| Невідомий тип | ? |
Object | null only |
Edge Cases
1. Nested PECS:
// Comparator — consumer
Comparator<? super T> comparator
// Фабрика — producer
public static <T> List<T> produce(List<? extends T> source) {
return new ArrayList<>(source);
}
// Комбінація
public static <T> void sort(List<T> list, Comparator<? super T> comp) {
list.sort(comp); // list — і read/write, comp — consumer
}
2. Stream API:
// Collector<T, A, R> — A це accumulator (consumer)
public static <T> Collector<T, ?, List<T>> toList() {
return Collectors.toList();
}
// ? — accumulator type невідомий (implementation detail)
3. Functional interfaces:
// Function — producer (повертає R)
Function<? super T, ? extends R>
// Consumer — consumer (приймає T)
Consumer<? super T>
// Supplier — producer (повертає T)
Supplier<? extends T>
Продуктивність
PECS:
- Runtime: Zero overhead
- Compile time: строга перевірка типів
- Безпека: запобігає ClassCastException
Продуктивність однакова для всіх wildcard типів
Production Experience
JDK приклади:
// Collections.copy — класичний PECS
public static <T> void copy(
List<? super T> dest, // consumer — записуємо T
List<? extends T> src // producer — читаємо 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();
}
Реальні кейи:
// Repository pattern
public interface Repository<T> {
// Producer — повертає дані
Optional<? extends T> findById(Long id);
List<? extends T> findAll();
// Consumer — приймає дані
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 для максимальної гнучкості
public static <T> void copy(List<? super T> dest, List<? extends T> src) { }
// ✅ Producer для читання
public double sum(List<? extends Number> numbers) { }
// ✅ Consumer для запису
public void fill(List<? super Integer> list, int value) { }
// ✅ Обидва — без wildcard
public <T> void swap(List<T> list, int i, int j) { }
// ❌ Extends для запису
// ❌ Super для читання конкретного типу
// ❌ Wildcard коли потрібно і read, і write
🎯 Шпаргалка для співбесіди
Обов’язково знати:
- PECS: Producer Extends — читаємо дані, Consumer Super — записуємо дані
List<? extends Number>— producer: read як Number, write забороненийList<? super Integer>— consumer: write Integer, read тільки як Object- Коли потрібно і read, і write — використовувати
<T>, без wildcard - JDK приклади:
Collections.copy(List<? super T>, List<? extends T>),Collections.max(Collection<? extends T>) - PECS — use-site variance, відрізняється від Kotlin
out T/in T(declaration-site)
Часті уточнюючі запитання:
- Наведіть приклад PECS з JDK? —
Collections.copy(dest, src),Stream.collect(Collector) - Коли wildcard НЕ потрібен? — Коли метод і читає, і пише (swap, reverse)
- Що таке nested PECS? —
Function<? super T, ? extends R>— T consumer, R producer - Чи можна додати null в List<? extends T>? — Так, null допустимий для будь-якого типу
Червоні прапорці (НЕ говорити):
- ❌ “PECS запобігає ClassCastException в runtime” — Це compile-time перевірка
- ❌ “Extends дозволяє записувати підтипи” — Extends забороняє запис повністю
- ❌ “Super дозволяє читати як T” — Super дозволяє читати тільки як Object
- ❌ “PECS — це патерн проектування” — Це мнемонічне правило для wildcard
Пов’язані теми:
- [[11. Що таке дженерики (Generics) в Java]]
- [[13. Що таке type erasure (стирання типів)]]
- [[16. У чому різниця між <? extends T> і <? super T>]]
- [[19. Що таке raw types і чому їх слід уникати]]