9

Что такое PECS (Producer Extends Consumer Super)?

9

Я столкнулся с концепцией PECS (сокращение от Producer extends and Consumer super) во время изучения дженериков в Java.

Может кто-нибудь объяснить, как использовать PECS, чтобы прояснить путаницу между extends и super?

5 ответ(ов)

10

Суть: "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.

0

Проблема, с которой вы сталкиваетесь, связана с концепциями ковариантности и контравариантности в 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> в зависимости от вашего контекста).

0

Вкратце, запомните три простых правила для PECS:

  1. Используйте <? extends T> wildcard, если вам нужно извлечь объекты типа T из коллекции.
  2. Используйте <? super T> wildcard, если вам нужно добавить объекты типа T в коллекцию.
  3. Если нужно удовлетворить оба условия, просто не используйте wildcard. Так просто.
0

В этой ситуации мы имеем следующую иерархию классов:

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 из списка. Добавлять элементы нельзя.

0

Поскольку примеров с дикими картами дженериков никогда не бывает достаточно, вот один из них.

// Исходные данные 
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 в коллекцию.

Таким образом, мы можем копировать элементы между коллекциями различных типов, соблюдая правила наследования.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь