Avatar ·

Что такое PECS (Producer Extends Consumer Super) в дженериках Java?

Всем привет! Я уже несколько месяцев пишу на Java, но до сих пор путаюсь с wildcard-ами в дженериках. Прочитал про PECS, но на практике всё равно не понимаю, когда использовать `? extends T`, а когда `? super T`. Вот конкретный пример, который меня сбивает с толку. У меня есть метод, который должен скопировать элементы из одного списка в другой: ```java public static void copy(List src, List dest) { for (Object item : src) { dest.add(item); // Ошибка компиляции! } } ``` Компилятор ругается на `dest.add(item)`. Я понимаю, что нужно использовать bounded wildcards, но не могу понять какие именно. Допустим, у меня есть иерархия классов: `Animal` → `Dog` → `Puppy`. Я хочу написать метод, который: 1. Читает объекты из списка (например, список `Dog`) 2. Записывает их в другой список (например, список `Animal`) Как правильно объявить типы для `src` и `dest`? И что вообще означает эта аббревиатура PECS? Буду благодарен за подробное объяснение с примерами, потому что в документации Oracle написано очень сухо. <<>>

Что такое 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 T
  • dest — потребитель (мы пишем в него) → используем ? 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-сообществе.

Еще один лайфхак: если сомневаетесь, задайте себе два вопроса:

  1. Буду ли я вызывать .add() на этой коллекции? Если да — нужен super.
  2. Буду ли я вызывать .get() и присваивать результат переменной определенного типа? Если да — нужен extends.

На практике в 90% случаев вы будете использовать ? extends T для параметров методов (читаете данные) и ? super T для возвращаемых значений или полей классов (куда нужно что-то положить).

И помните: если вы видите List<?> (unbounded wildcard), это означает "я не знаю тип, но могу только читать Object'ы". Это крайний случай, когда PECS не применим.