Что такое PECS (Producer Extends Consumer Super) в дженериках Java?
Что такое PECS и как это работает?
PECS — это мнемоническое правило, которое расшифровывается как Producer Extends, Consumer Super. Оно помогает запомнить, когда использовать ? extends T, а когда ? super T при работе с wildcard-ами в дженериках Java.
Давайте разберем на вашем примере с копированием списков. Представьте, что у нас есть иерархия классов:
class Animal {}
class Dog extends Animal {}
class Puppy extends Dog {}
Producer Extends (производитель — extends)
Если наш список производит (отдает) элементы, мы используем ? extends T. Это означает: "список содержит элементы какого-то типа, который является подтипом T или самим T".
Важно: из такого списка можно только читать, но нельзя добавлять элементы (кроме null). Почему? Потому что компилятор не знает точный тип элементов — это может быть Dog, Puppy или любой другой подтип Animal.
List<? extends Animal> producer = new ArrayList<Dog>();
Animal animal = producer.get(0); // ✅ Можно читать
producer.add(new Dog()); // ❌ Ошибка компиляции! Тип неизвестен
Consumer Super (потребитель — super)
Если список потребляет (принимает) элементы, мы используем ? super T. Это означает: "список может содержать элементы типа T или любого его супертипа".
В такой список можно добавлять элементы типа T или его подтипов, но при чтении мы гарантированно получим только Object.
List<? super Dog> consumer = new ArrayList<Animal>();
consumer.add(new Dog()); // ✅ Можно добавить Dog
consumer.add(new Puppy()); // ✅ Можно добавить Puppy (подтип Dog)
Object obj = consumer.get(0); // ✅ Читаем как Object
Dog dog = consumer.get(0); // ❌ Ошибка! Может быть Animal или Object
Решение вашей задачи
Теперь вернемся к вашему методу copy. Что здесь происходит?
src— производитель (мы читаем из него) → используем? extends Tdest— потребитель (мы пишем в него) → используем? super T
Правильная реализация:
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) { // Читаем элементы как T
dest.add(item); // Записываем в потребитель
}
}
// Пример использования:
List<Dog> dogs = Arrays.asList(new Dog(), new Puppy());
List<Animal> animals = new ArrayList<>();
copy(dogs, animals); // ✅ Работает!
Основные правила PECS
- Producer (вы только читаете данные) →
? extends T - Consumer (вы только записываете данные) →
? super T - И то, и другое (читаете и пишете) → используйте точный тип
Tбез wildcard-ов
Это правило применяется не только к коллекциям, но и к любым обобщенным типам. Например, Comparator<? super T> или Supplier<? extends T>.
Отличное объяснение от коллеги выше. Хочу добавить еще один практический совет, который помогает не путаться в PECS.
Запомните фразу: "если вы кладете — super, если достаете — extends". Но есть нюанс: если вы делаете и то, и другое — используйте конкретный тип без wildcard.
Вот еще один распространенный пример из стандартной библиотеки Java — метод Collections.copy():
public static <T> void copy(List<? super T> dest, List<? extends T> src)
Обратите внимание на порядок параметров: сначала потребитель (dest), потом производитель (src). Это не случайно — так принято в Java-сообществе.
Еще один лайфхак: если сомневаетесь, задайте себе два вопроса:
- Буду ли я вызывать
.add()на этой коллекции? Если да — нуженsuper. - Буду ли я вызывать
.get()и присваивать результат переменной определенного типа? Если да — нуженextends.
На практике в 90% случаев вы будете использовать ? extends T для параметров методов (читаете данные) и ? super T для возвращаемых значений или полей классов (куда нужно что-то положить).
И помните: если вы видите List<?> (unbounded wildcard), это означает "я не знаю тип, но могу только читать Object'ы". Это крайний случай, когда PECS не применим.