Что такое 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 и почему их следует избегать]]