Что такое принцип PECS в Java? - Вопросы

Всего: 7 вопросов

1. 

Что такое принцип PECS в Java?

PECS (Producer Extends, Consumer Super) — правило выбора границы wildcard для параметра обобщённого типа. Решение принимается по роли параметра в методе:

  • параметр — producer (источник), метод только читает из него элементы T → <? extends T>;
  • параметр — consumer (приёмник), метод только записывает в него T → <? super T>;
  • метод и читает, и пишет → точный тип T без wildcard.

Короткая форма: достаёте — extends, кладёте — super. Название ввёл Джошуа Блох в «Effective Java» (3-е издание, Item 31). Пример: метод sum(List<Number>) не примет List<Integer>, потому что обобщённые типы инвариантны, а sum(List<? extends Number>) примет — список только читается.

2. 

Какие списки можно передать в параметр List<? extends Employee> и List<? super Employee> и что с ними можно делать внутри метода?

Для иерархии Person ← Employee ← Manager:

  • List<? extends Employee> (upper bounded, producer) принимает List<Employee> и List<Manager>. get() возвращает Employee, а добавить можно только null: add(new Employee()) даёт incompatible types: Employee cannot be converted to CAP#1, ведь за параметром может стоять List<Manager>.
  • List<? super Employee> (lower bounded, consumer) принимает List<Employee>, List<Person>, List<Object>. Добавлять можно Employee, Manager и null, а get() возвращает только Object — дальше нужна проверка instanceof.
  • List<?> принимает любой список, читает элементы как Object, добавить можно только null. Этим он отличается от List<Object>, в который добавлять можно что угодно, но передать List<String> нельзя.
3. 

Почему в параметр List<? super Employee> можно передать List<Person>, но нельзя выполнить add(new Person())?

Это не исключение из PECS, а два следствия одной границы. Компилятор разрешает только операции, безопасные для любого списка, который может стоять за ? super Employee: List<Employee>, List<Person> или List<Object>.

  • add(new Employee()) и add(new Manager()) безопасны: такой объект одновременно Employee, Person и Object.
  • add(new Person()) подошёл бы к List<Person>, но если передан List<Employee>, в него попал бы объект, не являющийся Employee. Компилятор не знает, какой список передан, и запрещает операцию.

Та же граница действует и при передаче аргумента, и при присваивании (List<? super Employee> list = persons; компилируется, = managers; — нет), и при добавлении элементов. Поэтому утверждение «PECS работает только при присваивании коллекций и не работает при добавлении» неверно.

4. 

Как применить PECS в собственном методе, который переносит элементы из одного списка в другой?

PECS применяется к каждому параметру отдельно: источник получает extends, приёмник — super.

// candidates — producer, staff — consumer
static void hireAll(List<? extends Employee> candidates, List<? super Employee> staff) {
    for (Employee e : candidates) {
        staff.add(e);
    }
}

List<Manager> managers = List.of(new Manager(), new Manager());
List<Person> persons = new ArrayList<>();
hireAll(managers, persons);
System.out.println(persons.size()); // 2

Если объявить оба параметра как List<Employee>, вызов hireAll(managers, persons) не скомпилируется ни по первому, ни по второму аргументу. С wildcard метод принимает максимум сочетаний типов и остаётся типобезопасным.

5. 

Как принцип PECS виден в сигнатурах стандартной библиотеки Java?

Методы JDK, которые читают из одного параметра и пишут в другой, объявлены по PECS:

  • Collections.copy(List<? super T> dest, List<? extends T> src) — src producer, dest consumer;
  • Collections.addAll(Collection<? super T> c, T... elements) — коллекция consumer;
  • Collection.addAll(Collection<? extends E> c) — аргумент producer;
  • List.sort(Comparator<? super E> c) — компаратор consumer элементов;
  • Collections.max(Collection<? extends T> coll, Comparator<? super T> comp) — коллекция producer, компаратор consumer.

Comparator<T> принимает объекты в compare(), то есть потребляет их, поэтому его объявляют как ? super T. Благодаря этому один Comparator<Object>, например Comparator.comparing(Object::toString), сортирует и List<Integer>: [10, 9, 100] превращается в [10, 100, 9] (сравнение строк).

6. 

Почему Collections.copy() не работает с пустым списком-приёмником, хотя типы по PECS подходят?

PECS отвечает только за типы, а не за размер. Collections.copy(dest, src) заменяет элементы dest по индексам и не добавляет новые: если dest.size() < src.size(), бросается IndexOutOfBoundsException: Source does not fit in dest. Если dest длиннее, хвост остаётся: копирование [3, 1, 2] в List<Number> [0, 0, 0, 0.5] даёт [3, 1, 2, 0.5].

Для копирования в пустой список используйте dest.addAll(src) или new ArrayList<>(src).

7. 

Какие ошибки чаще всего допускают при использовании PECS и wildcard?

  • Ожидать, что List<Manager> — подтип List<Employee>. Обобщённые типы инвариантны; параметр-источник объявляют как List<? extends Employee>.
  • Добавлять в List<? extends T> — ошибка cannot be converted to CAP#1. Если метод пишет в коллекцию, нужен ? super T или точный T.
  • Считать ? extends защитой от изменений: remove(), clear() и add(null) работают. Для неизменяемости — List.copyOf() или Collections.unmodifiableList().
  • Читать из List<? super T> как T — элемент имеет тип Object. Если нужно и читать как T, и писать — точный тип List<T>.
  • Ставить wildcard в возвращаемый тип (List<? extends Number> load()): вызывающий код не сможет ничего добавить в результат. Wildcard — только в параметрах.
  • Пытаться задать две границы: <? extends A super B> не компилируется — у wildcard одна граница.
Страница 1 из 1