Что такое PECS (Producer Extends Consumer Super)?
Я столкнулся с концепцией PECS (сокращение от Producer extends
and Consumer super
) во время изучения дженериков в Java.
Может кто-нибудь объяснить, как использовать PECS, чтобы прояснить путаницу между extends
и super
?
5 ответ(ов)
Суть: "PECS" рассматривает коллекцию с точки зрения ее использования. Если вы только извлекаете элементы из обобщенной коллекции, она является производителем, и вам следует использовать extends
; если вы только добавляете элементы, она является потребителем, и вам следует использовать super
. Если вы делаете и то, и другое с одной и той же коллекцией, не используйте ни extends
, ни super
.
Предположим, у вас есть метод, который принимает в качестве параметра коллекцию объектов, но вы хотите, чтобы он был более гибким, чем просто принятие Collection<Thing>
.
Случай 1: Вы хотите перебрать коллекцию и выполнить операции с каждым элементом.
В этом случае коллекция является производителем, поэтому вам следует использовать Collection<? extends Thing>
.
Аргументация заключается в том, что Collection<? extends Thing>
может содержать любой подкласс Thing
, и, следовательно, каждый элемент будет вести себя как Thing
, когда вы выполняете свою операцию. (На самом деле вы не можете добавлять ничего (кроме null) в Collection<? extends Thing>
, поскольку вы не можете знать во время выполнения, какой конкретный подкласс Thing
хранится в коллекции.)
Случай 2: Вы хотите добавлять элементы в коллекцию.
В этом случае коллекция является потребителем, и вам следует использовать Collection<? super Thing>
.
Аргументация здесь такова: в отличие от Collection<? extends Thing>
, Collection<? super Thing>
всегда может содержать Thing
, независимо от того, какой фактический параметризованный тип указан. Здесь вам не важно, что уже находится в списке, пока он позволяет добавить Thing
; это и гарантирует ? super Thing
.
Проблема, с которой вы сталкиваетесь, связана с концепциями ковариантности и контравариантности в Java.
В методе testCoVariance(List<? extends B> myBlist)
вы используете оператор ? extends B
, что указывает на то, что myBlist
может содержать объекты, которые являются типами B
или его подклассами. Однако это также означает, что вы не можете добавлять элементы в список, так как компилятор не может гарантировать, что добавляемый элемент будет совместим с конкретным подтипом, содержащимся в списке. В результате, строка myBlist.add(b);
и myBlist.add(c);
не компилируются.
Теперь, в методе testContraVariance(List<? super B> myBlist)
, ? super B
позволяет добавлять объекты типа B
и его подклассов в список. Таким образом, строки myBlist.add(b);
и myBlist.add(c);
компилируются нормально. Однако, когда вы пытаетесь получить элемент из списка с помощью myBlist.get(0);
, это также приведет к ошибке компиляции, так как компилятор не знает, какого точного типа объекты находятся в списке, и не может гарантировать, что это действительно объекты типа B
или его подклассов.
Подведем итоги:
- В
testCoVariance
вы можете получать элементы, но не можете добавлять, так как используете? extends B
. - В
testContraVariance
вы можете добавлять элементы, но не можете гарантировать их тип при получении, так как используете? super B
.
Если вам нужно как добавлять, так и получать элементы, вам стоит рассмотреть возможность использования обобщенных типов без ограничения (например, List<B>
или List<A>
в зависимости от вашего контекста).
Вкратце, запомните три простых правила для PECS:
- Используйте
<? extends T>
wildcard, если вам нужно извлечь объекты типаT
из коллекции. - Используйте
<? super T>
wildcard, если вам нужно добавить объекты типаT
в коллекцию. - Если нужно удовлетворить оба условия, просто не используйте wildcard. Так просто.
В этой ситуации мы имеем следующую иерархию классов:
class Creature{} // X
class Animal extends Creature{} // Y
class Fish extends Animal{} // Z
class Shark extends Fish{} // A
class HammerShark extends Shark{} // B
class DeadHammerShark extends HammerShark{} // C
Давайте проясним PE - Producer Extends:
Когда мы пишем:
List<? extends Shark> sharks = new ArrayList<>();
смысл в том, что переменная sharks
может хранить список, элементы которого могут быть любого подтипа класса Shark
, а именно Shark
, HammerShark
или DeadHammerShark
. Однако вы не можете добавлять в этот список объекты классов, производных от Shark
, так как:
sharks.add(new HammerShark()); // вызовет ошибку компиляции
Причина заключается в том, что на этапе компиляции компилятор не может гарантировать, что sharks
будет хранить только элементы одного конкретного подтипа Shark
в момент выполнения времени. Например, если ваш список на самом деле инициализирован как List<DeadHammerShark>
, а вы попытаетесь добавить объект типа HammerShark
, это приведет к ошибке, так как вы не можете добавлять объекты супертипа в список, который предназначен для хранения объектов подтипа. В итоге, даже если на этапе компиляции видно, что вы добавляете HammerShark
, во время выполнения это может вызвать ситуацию, при которой вы попытаетесь добавить супертип объекта.
Если вы спросите: "Но почему я не могу добавить HammerShark
, если это самый низкий по иерархии тип?" Ответ в том, что это самый низкий тип, который вам известен. На самом деле, кто-то другой может создать новый класс, производный от HammerShark
, и вы опять окажетесь в той же ситуации.
Давайте проясним CS - Consumer Super:
Теперь рассмотрим следующее:
List<? super Shark> sharks = new ArrayList<>();
Что и почему вы можете добавить в этот список?
sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerShark());
Вы можете добавлять эти типы объектов, потому что все, что ниже Shark
(то есть A
, B
и C
), всегда будет подтипами любого из типов выше Shark
(то есть X
, Y
, Z
). Это просто и понятно.
Вы не можете добавлять объекты, которые выше Shark
, потому что во время выполнения тип добавляемого объекта может быть выше иерархии, чем объявленный тип списка (X
, Y
, Z
). Это запрещено.
Но почему вы не можете извлекать из этого списка значения?
(Я имею в виду, что вы можете получить элемент, но не можете присвоить его переменной, отличной от Object o
):
Object o;
o = sharks.get(2); // единственное, что работает
Animal s;
s = sharks.get(2); // не работает
Во время исполнения тип списка может быть любым из типов выше A
: X
, Y
, Z
и так далее. Компилятор может скомпилировать вашу строку присваивания (что кажется правильным), но во время выполнения тип s
(который Animal
) может оказаться ниже в иерархии, чем объявленный тип списка (который может быть Creature
или выше) — это не допускается.
В заключение:
Мы используем <? super T>
для добавления объектов типов, равных или ниже T
в список. Чтение объектов невозможно.
Мы используем <? extends T>
для чтения объектов типов, равных или ниже T
из списка. Добавлять элементы нельзя.
Поскольку примеров с дикими картами дженериков никогда не бывает достаточно, вот один из них.
// Исходные данные
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(2.78, 3.14);
List<Number> numList = Arrays.asList(1, 2, 2.78, 3.14, 5);
// Место назначения
List<Integer> intList2 = new ArrayList<>();
List<Double> doublesList2 = new ArrayList<>();
List<Number> numList2 = new ArrayList<>();
// Работает
copyElements1(intList, intList2); // копируем из int в int
copyElements1(doubleList, doublesList2); // копируем из double в double
static <T> void copyElements1(Collection<T> src, Collection<T> dest) {
for (T n : src) {
dest.add(n);
}
}
// Попробуем скопировать intList в его супертип
copyElements1(intList, numList2); // ошибка, сигнатура метода просто говорит "T"
// и здесь компилятору даны два типа: Integer и Number,
// так какой из них выбрать?
// PECS на помощь!
copyElements2(intList, numList2); // работает
// копируем Integer (? extends T) в его супертип (Number — суперкласс Integer)
private static <T> void copyElements2(Collection<? extends T> src,
Collection<? super T> dest) {
for (T n : src) {
dest.add(n);
}
}
В этом примере мы видим использование диких карт дженериков (wildcards) в Java. В случае метода copyElements1
, мы сталкиваемся с проблемой компиляции, так как компилятор не знает, как интерпретировать два различных типа — Integer
и Number
. Вместо этого мы можем воспользоваться принципом PECS (Producer Extends, Consumer Super), который позволяет нам определить, какие типы могут быть использованы для источников и назначения. Метод copyElements2
принимает Collection<? extends T>
для источника, что позволяет передавать коллекции, содержащие элементы, которые являются подклассами T
, и Collection<? super T>
для назначения, что позволяет добавлять объекты T
в коллекцию.
Таким образом, мы можем копировать элементы между коллекциями различных типов, соблюдая правила наследования.
Разница между <? super T> и <? extends T> в Java
Как создать обобщённый массив в Java?
Является ли List<Собака> подклассом List<Животное>? Почему дженерики в Java не являются неявно полиморфными?
Как получить экземпляр класса обобщенного типа T?
Как работают сервлеты? Инстанцирование, сессии, общие переменные и многопоточность