Что такое PECS (продюсер продвигает Consumer Super)?


729

Я сталкивался с PECS (сокращение от Producer extendsи Consumersuper ), читая дженерики.

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


3
Очень хорошее объяснение с примером @ youtube.com/watch?v=34oiEq9nD0M&feature=youtu.be&t=1630, который объясняет superчасть, но дает представление о другой.
Лупчиазоем

Ответы:


844

tl; dr: «PECS» с точки зрения коллекции. Если вы берете только предметы из общей коллекции, это производитель, и вы должны использовать его extends; если вы только набиваете предметы, это потребитель, и вы должны его использовать super. Если вы делаете оба с одной коллекцией, вы не должны использовать либо extendsили super.


Предположим, у вас есть метод, который принимает в качестве параметра набор вещей, но вы хотите, чтобы он был более гибким, чем просто принятие a Collection<Thing>.

Случай 1: Вы хотите просмотреть коллекцию и сделать что-то с каждым предметом.
Тогда список является производителем , поэтому вы должны использовать Collection<? extends Thing>.

Причина заключается в том, что a Collection<? extends Thing>может содержать любой подтип Thing, и поэтому каждый элемент будет вести себя как a, Thingкогда вы выполняете свою операцию. (На самом деле вы ничего не можете добавить к a Collection<? extends Thing>, потому что вы не можете знать во время выполнения, какой конкретный подтип Thingколлекции содержит.)

Случай 2: Вы хотите добавить вещи в коллекцию.
Тогда список является потребителем , поэтому вы должны использовать Collection<? super Thing>.

Причиной здесь является то Collection<? extends Thing>, что в отличие от этого , Collection<? super Thing>всегда может храниться Thingнезависимо от того, что является фактическим параметризованным типом. Здесь вас не волнует, что уже есть в списке, если это позволит добавить a Thing; это то, что ? super Thingгарантирует.


142
Я всегда пытаюсь думать об этом так: производителю разрешено производить что-то более конкретное, следовательно, расширяется , потребителю разрешается принимать что-то более общее, а значит, супер .
Feuermurmel

10
Еще один способ запомнить разницу между производителем и потребителем - подумать о сигнатуре метода. Если у вас есть метод doSomethingWithList(List list), вы потребляя список и поэтому нужно будешь ковариация / продолжается (или инвариантное List). С другой стороны , если ваш метод List doSomethingProvidingList, то вы производить этот список и будет нуждаться в контравариации / супер (или инвариантное List).
Раман

3
@MichaelMyers: Почему мы не можем просто использовать параметризованный тип для обоих этих случаев? Есть ли здесь какое-то конкретное преимущество использования подстановочных знаков или это просто средство улучшения читабельности, подобное, скажем, использованию ссылок на constпараметры метода в C ++ для обозначения того, что метод не изменяет аргументы?
Чаттерджи

7
@ Раман, я думаю, ты просто запутался. В doSthWithList (у вас может быть List <? Super Thing>), поскольку вы являетесь потребителем, вы можете использовать super (помните, CS). Тем не менее, это список <? extends Thing> getList (), поскольку вам разрешено возвращать что-то более конкретное при создании (PE).
masterxilo

4
@AZ_ Я разделяю твои чувства. Если метод действительно получает () из списка, метод считается Consumer <T>, а список считается поставщиком; но правило PECS «с точки зрения списка», поэтому требуется «расширяет». Это должно быть GEPS: получить расширяет; поставить супер.
Treefish Чжан

561

Принципы, стоящие за этим в информатике называется

  • Ковариации: ? extends MyClass,
  • Контравариантность: ? super MyClassи
  • Инвариантность / не отклонение: MyClass

Картинка ниже должна объяснить концепцию. Фото предоставлено Андреем Тюкиным

Ковариантность против Контравариантности


144
Всем привет. Я Андрей Тюкин, я просто хотел подтвердить, что anoopelias & DaoWen связались со мной и получили мое разрешение на использование скетча, он лицензирован по (CC) -BY-SA. Thx @ Anoop за то, что он дал ему вторую жизнь ^^ @Brian Agnew: (на "несколько голосов"): это потому, что это эскиз для Scala, он использует синтаксис Scala и предполагает дисперсию на сайте объявлений, которая довольно сильно отличается от странного вызова Java -согласование ... Может быть, я должен написать более подробный ответ, который ясно показывает, как этот набросок применим к Java ...
Андрей Тюкин

3
Это одно из самых простых и ясных объяснений Ковариации и Контравариантности, которые я когда-либо нашел!
cs4r

@ Андрей Тюкин Привет, я тоже хочу использовать это изображение. Как я могу связаться с вами?
slouc

Если у вас есть какие-либо вопросы об этой иллюстрации, мы можем обсудить их в чате: chat.stackoverflow.com/rooms/145734/…
Андрей Тюкин


49

PECS (Производитель extendsи Потребитель super)

мнемоника → принцип «получи и положи».

Этот принцип гласит, что:

  • Используйте подстановочный знак extends, когда вы получаете значения только из структуры.
  • Используйте супер подстановочный знак, когда вы только помещаете значения в структуру.
  • И не используйте подстановочный знак, когда вы оба получаете и ставите.

Пример на Java:

class Super {

    Object testCoVariance(){ return null;} //Covariance of return types in the subtype.
    void testContraVariance(Object parameter){} // Contravariance of method arguments in the subtype.
}

class Sub extends Super {

    @Override
    String testCoVariance(){ return null;} //compiles successfully i.e. return type is don't care(String is subtype of Object) 
    @Override
    void testContraVariance(String parameter){} //doesn't support even though String is subtype of Object

}

Принцип подстановки Лискова: если S является подтипом T, то объекты типа T могут быть заменены объектами типа S.

В системе типов языка программирования, правило ввода

  • ковариантный, если он сохраняет порядок типов (≤), который упорядочивает типы от более специфических к более общим;
  • контравариантно, если оно отменяет этот порядок;
  • инвариантный или неизменный, если ни один из них не применим.

Ковариация и контравариантность

  • Типы данных (источники) только для чтения могут быть ковариантными ;
  • Только для записи типы данных (приемники) могут быть контравариантными .
  • Изменяемые типы данных, которые действуют как источники и приемники, должны быть инвариантными .

Чтобы проиллюстрировать это общее явление, рассмотрим тип массива. Для типа Animal мы можем сделать тип Animal []

  • ковариант : кот [] - это животное [];
  • противоречивый : животное [] - это кошка [];
  • инвариант : животное [] не является котом [], а кот [] не является животным [].

Примеры Java:

Object name= new String("prem"); //works
List<Number> numbers = new ArrayList<Integer>();//gets compile time error

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //attempt of heap pollution i.e. at runtime gets java.lang.ArrayStoreException: java.lang.Double(we can fool compiler but not run-time)

List<String> list=new ArrayList<>();
list.add("prem");
List<Object> listObject=list; //Type mismatch: cannot convert from List<String> to List<Object> at Compiletime  

больше примеров

ограниченный (т.е. направленный куда-то) подстановочный знак : есть 3 различных вида подстановочных знаков:

  • In-дисперсия / Non-дисперсия: ?или ? extends Object- Неограниченный подстановочный знак. Это обозначает семью всех типов. Используйте, когда вы оба получите и положите.
  • Co-дисперсия: ? extends T(семейство всех типов, являющихся подтипами T) - подстановочный знак с верхней границей . Tсамый верхний класс в иерархии наследования. Используйте extendsподстановочный знак, когда вы только получаете значения из структуры.
  • Противопоставление: ? super T(семейство всех типов, являющихся супертипами T) - подстановочный знак с нижней границей . Tсамый нижний класс в иерархии наследования. Используйте superподстановочный знак, когда вы только помещаете значения в структуру.

Примечание: подстановочный знак ?означает ноль или один раз , представляет неизвестный тип. Подстановочный знак может использоваться как тип параметра, никогда не использоваться в качестве аргумента типа для вызова универсального метода, создания экземпляра универсального класса (т. Е. Когда используется подстановочный знак, ссылка на который не используется в других частях программы, как мы используем T)

введите описание изображения здесь

class Shape { void draw() {}}

class Circle extends Shape {void draw() {}}

class Square extends Shape {void draw() {}}

class Rectangle extends Shape {void draw() {}}

public class Test {
 /*
   * Example for an upper bound wildcard (Get values i.e Producer `extends`)
   * 
   * */  

    public void testCoVariance(List<? extends Shape> list) {
        list.add(new Shape()); // Error:  is not applicable for the arguments (Shape) i.e. inheritance is not supporting
        list.add(new Circle()); // Error:  is not applicable for the arguments (Circle) i.e. inheritance is not supporting
        list.add(new Square()); // Error:  is not applicable for the arguments (Square) i.e. inheritance is not supporting
        list.add(new Rectangle()); // Error:  is not applicable for the arguments (Rectangle) i.e. inheritance is not supporting
        Shape shape= list.get(0);//compiles so list act as produces only

        /*You can't add a Shape,Circle,Square,Rectangle to a List<? extends Shape> 
         * You can get an object and know that it will be an Shape
         */         
    }
      /* 
* Example for  a lower bound wildcard (Put values i.e Consumer`super`)
* */
    public void testContraVariance(List<? super Shape> list) {
        list.add(new Shape());//compiles i.e. inheritance is supporting
        list.add(new Circle());//compiles i.e. inheritance is  supporting
        list.add(new Square());//compiles i.e. inheritance is supporting
        list.add(new Rectangle());//compiles i.e. inheritance is supporting
        Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer
        Object object= list.get(0); // gets an object, but we don't know what kind of Object it is.

        /*You can add a Shape,Circle,Square,Rectangle to a List<? super Shape> 
        * You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
        */  
    }
}

дженерики и примеры


Эй, я просто хотел узнать, что вы имели в виду в последнем предложении: «Если вы считаете, что моя аналогия неверна, пожалуйста, обновите». Вы имеете в виду, если это этически неправильно (это субъективно) или если это неправильно в контексте программирования (что объективно: нет, это не так)? Я хотел бы заменить его более нейтральным примером, который является универсально приемлемым, независимо от культурных норм и этических убеждений; Если это нормально с тобой.
Нейрон

наконец я мог получить это. Хорошее объяснение.
Олег Куц

2
@Premraj, In-variance/Non-variance: ? or ? extends Object - Unbounded Wildcard. It stands for the family of all types. Use when you both get and put.я не могу добавить элемент в список <?> Или список <? расширяет объект>, поэтому я не понимаю, почему это может быть Use when you both get and put.
LiuWenbin_NO.

1
@LiuWenbin_NO. - Эта часть ответа вводит в заблуждение. ?- «неограниченный подстановочный знак» - соответствует полной противоположности инвариантности. Пожалуйста, обратитесь к следующей документации: docs.oracle.com/javase/tutorial/java/generics/…, которая гласит: В случае, когда коду требуется доступ к переменной как к переменным «in», так и к «out», выполните не используйте подстановочный знак. (Они используют «in» и «out» как синонимы «get» и «put»). За исключением того, что nullвы не можете добавить в коллекцию с параметром ?.
mouselabs

29
public class Test {

    public class A {}

    public class B extends A {}

    public class C extends B {}

    public void testCoVariance(List<? extends B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b); // does not compile
        myBlist.add(c); // does not compile
        A a = myBlist.get(0); 
    }

    public void testContraVariance(List<? super B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b);
        myBlist.add(c);
        A a = myBlist.get(0); // does not compile
    }
}

Так что «? Расширяет B» следует интерпретировать как «? Расширяет B». Это то, что B расширяет, так что будет включать в себя все суперклассы B вплоть до Object, за исключением самого B. Спасибо за код!
Саураб Патил

3
@SaurabhPatil Нет, ? extends Bозначает B и все, что расширяет B.
спрашивает

24

Как я объясняю в своем ответе на другой вопрос, PECS - это мнемоническое устройство, созданное Джошем Блохом, чтобы помочь вспомнить P roducer extends, C onsumer super.

Это означает, что когда параметризованный тип, передаваемый методу, будет генерировать экземпляры T(они будут каким-то образом извлечены из него), его ? extends Tследует использовать, поскольку любой экземпляр подкласса Tтакже является a T.

Когда параметризованный тип, передаваемый методу, будет потреблять экземпляры T(они будут переданы ему для выполнения чего-либо), его ? super Tследует использовать, поскольку экземпляр Tможно легально передать любому методу, который принимает некоторый супертип T. Например, Comparator<Number>можно использовать на Collection<Integer>. ? extends Tне будет работать, потому что Comparator<Integer>не может работать на Collection<Number>.

Обратите внимание , что , как правило , вы должны быть только с помощью ? extends Tи ? super Tдля параметров некоторого метода. Методы должны просто использоваться Tв качестве параметра типа общего возвращаемого типа.


1
Этот принцип действует только для коллекций? Это имеет смысл, когда кто-то пытается сопоставить его со списком. Если вы думаете о сигнатуре sort (List <T>, Comparator <? Super T>) ---> здесь Comparator использует super, поэтому это означает, что он является потребителем в контексте PECS. Когда вы смотрите на реализацию, например: public int Compare (Person a, Person b) {return a.age <b.age? -1: a.age == b.age? 0: 1; } Я чувствую, что человек ничего не потребляет, но рождает возраст. Это меня смущает. Есть ли в моих рассуждениях недостаток или PECS действует только для коллекций?
Фатих Арслан

24

Короче говоря, три простых правила, чтобы запомнить PECS:

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

10

давайте предположим эту иерархию:

class Creature{}// X
class Animal extends Creature{}// Y
class Fish extends Animal{}// Z
class Shark extends Fish{}// A
class HammerSkark extends Shark{}// B
class DeadHammerShark extends HammerSkark{}// C

Давайте уточним PE - Производитель расширяет:

List<? extends Shark> sharks = new ArrayList<>();

Почему вы не можете добавить объекты, которые расширяют "Акула" в этом списке? подобно:

sharks.add(new HammerShark());//will result in compilation error

Поскольку у вас есть список, который может быть типа A, B или C во время выполнения , вы не можете добавить в него любой объект типа A, B или C, потому что вы можете получить комбинацию, которая не разрешена в Java.
На практике компилятор действительно может видеть во время компиляции, что вы добавляете B:

sharks.add(new HammerShark());

... но он не может определить, будет ли во время выполнения ваш B подтипом или супертипом типа списка. Во время выполнения тип списка может быть любым из типов A, B, C. Таким образом, вы не можете добавить HammerSkark (супертип) в список, например, DeadHammerShark.

* Вы скажете: «Хорошо, но почему я не могу добавить в него HammerSkark, так как это самый маленький тип?». Ответ: это самое маленькое, что вы знаете. Но HammerSkark может быть расширен кем-то другим, и вы в конечном итоге в том же сценарии.

Давайте поясним CS - Consumer Super:

В той же иерархии мы можем попробовать это:

List<? super Shark> sharks = new ArrayList<>();

Что и почему вы можете добавить в этот список?

sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());

Вы можете добавить вышеупомянутые типы объектов, потому что все, что находится ниже акулы (A, B, C), всегда будет подтипом чего-либо, что находится выше акулы (X, Y, Z). Легко понять.

Вы не можете добавлять типы выше Shark, потому что во время выполнения тип добавленного объекта может быть выше в иерархии, чем объявленный тип списка (X, Y, Z). Это не разрешено

Но почему вы не можете прочитать из этого списка? (Я имею в виду, что вы можете извлечь из него элемент, но вы не можете назначить его чему-либо, кроме Object o):

Object o;
o = sharks.get(2);// only assignment that works

Animal s;
s = sharks.get(2);//doen't work

Во время выполнения тип списка может быть любого типа выше A: X, Y, Z, ... Компилятор может скомпилировать ваш оператор присваивания (который кажется правильным), но во время выполнения тип s (Animal) может быть ниже в иерархия, чем объявленный тип списка (который может быть Существо или выше). Это не разрешено

Подводить итоги

Мы используем <? super T>для добавления объектов типов, равных или ниже Tк List. Мы не можем читать из этого.
Мы используем <? extends T>для чтения объектов типов, равных или ниже Tиз списка. Мы не можем добавить элемент к нему.


9

(добавив ответ, потому что примеров с подстановочными знаками Generics недостаточно)

       // Source 
       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);

       // Destination
       List<Integer> intList2 = new ArrayList<>();
       List<Double> doublesList2 = new ArrayList<>();
       List<Number> numList2 = new ArrayList<>();

        // Works
        copyElements1(intList,intList2);         // from int to int
        copyElements1(doubleList,doublesList2);  // from double to double


     static <T> void copyElements1(Collection<T> src, Collection<T> dest) {
        for(T n : src){
            dest.add(n);
         }
      }


     // Let's try to copy intList to its supertype
     copyElements1(intList,numList2); // error, method signature just says "T"
                                      // and here the compiler is given 
                                      // two types: Integer and Number, 
                                      // so which one shall it be?

     // PECS to the rescue!
     copyElements2(intList,numList2);  // possible



    // copy Integer (? extends T) to its supertype (Number is super of Integer)
    private static <T> void copyElements2(Collection<? extends T> src, 
                                          Collection<? super T> dest) {
        for(T n : src){
            dest.add(n);
        }
    }

4

Это самый ясный и простой способ для меня думать о расширяется против супер:

  • extendsдля чтения

  • superдля написания

Я считаю, что «PECS» - это неочевидный способ думать о том, кто является «производителем», а кто «потребителем». «PECS» определяется с точки зрения самого сбора данных - коллекция «потребляет», если в нее записываются объекты (она потребляет объекты из вызывающего кода), и «выдает», если объекты читаются из нее (это создает объекты для некоторого вызывающего кода). Это противоречит тому, как все остальное называется. Стандартные API Java названы с точки зрения вызывающего кода, а не самой коллекции. Например, представление java.util.List, ориентированное на коллекцию, должно иметь метод с именем «receive ()» вместо «add ()» - в конце концов,элемент, но сам список получает элемент.

Я думаю, что более интуитивно, естественно и непротиворечиво думать о вещах с точки зрения кода, который взаимодействует с коллекцией - код «читает из» или «записывает» в коллекцию? После этого любой код, записывающий в коллекцию, будет «производителем», а любой код, читающий из коллекции, будет «потребителем».


Я столкнулся с тем же психическим столкновением и буду склонен согласиться разве что PECS не указует именование кода и границу типа сами являются установленной на декларациях коллекции. Более того, что касается именования, у вас часто есть имена для создания / потребления Коллекций, таких как srcи dst. Таким образом, вы имеете дело как с кодом, так и с контейнерами одновременно, и я в конечном итоге задумался над этим следующим образом: «потребляющий код» потребляет из производящего контейнера, а «производящий код» производит для потребляющего контейнера.
mouselabs

4

«Правило» PECS гарантирует, что следующее является законным:

  • Потребитель: что бы ?это ни было , это может юридически относиться к T
  • Производитель: все , что ?есть, это может быть законно ссылаются T

Типичное спаривание по типу List<? extends T> producer, List<? super T> consumerпросто гарантирует, что компилятор может применять стандартные правила отношений наследования "IS-A". Если бы мы могли сделать это легально, было бы проще сказать <T extends ?>, <? extends T>(или еще лучше в Scala, как вы можете видеть выше, это так [-T], [+T]. К сожалению, лучшее, что мы можем сделать, это <? super T>, <? extends T>.

Когда я впервые столкнулся с этим и сломал его в своей голове, механика имела смысл, но сам код продолжал казаться мне запутанным - я продолжал думать, что «кажется, что границы не должны быть так перевернуты» - хотя я По вышесказанному было ясно - что речь идет просто о гарантии соблюдения стандартных правил ведения.

Что помогло мне, так это обычное задание в качестве аналогии.

Рассмотрим следующий (не готовый к производству) игрушечный код:

// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
   for(T t : producer)
       consumer.add(t);
}

Иллюстрируя это с точки зрения аналогии назначения, для consumerв ?подстановки (неизвестный тип) ссылка - «левая сторона» на уступки - и <? super T>гарантирует , что все ?это, T«IS-A» ?- что Tможет быть возложены на него, потому что ?это супер тип (или не более того же типа), как T.

Для producerконцерна это то же самое , это просто перевернутая: producer«s ?подстановочные (неизвестный тип) является референтом - в„правую“выполнения задания - и <? extends T>гарантирует , что все ?это, ?„IS-A“ T- что он может быть назначен на аT , потому что ?это подтип (или, по крайней мере, тот же тип), что и T.


2

Запомните это:

Потребитель ест ужин (супер); Продюсер расширяет фабрику своих родителей


1

Используя пример из реальной жизни (с некоторыми упрощениями):

  1. Представьте грузовой поезд с грузовыми вагонами в качестве аналогии с перечнем.
  2. Вы можете поместить груз в грузовой вагон, если груз имеет такой же или меньший размер, чем грузовой вагон =<? super FreightCarSize>
  3. Вы можете выгрузить груз из грузового вагона, если у вас достаточно места (больше, чем размер груза) в вашем депо =<? extends DepotSize>

1

Ковариантность : принять подтипы.
Контравариантность : принять подтипы .

Ковариантные типы доступны только для чтения, а контравариантные - только для записи.


0

Давайте посмотрим на пример

public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }

Обобщения позволяют безопасно и динамически работать с типами.

//ListA
List<A> listA = new ArrayList<A>();

//add
listA.add(new A());
listA.add(new B());
listA.add(new C());

//get
A a0 = listA.get(0);
A a1 = listA.get(1);
A a2 = listA.get(2);
//ListB
List<B> listB = new ArrayList<B>();

//add
listB.add(new B());

//get
B b0 = listB.get(0);

проблема

Поскольку коллекция Java является ссылочным типом, в результате мы имеем следующие проблемы:

Проблема № 1

//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;

* У универсала Swift такой проблемы нет, потому что Collection - это Value type[About], поэтому создается новая коллекция.

Проблема № 2

//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;

Решение - универсальные шаблоны

Подстановочный знак является признаком ссылочного типа, и его нельзя создать напрямую

Решение # 1, также <? super A> известное как нижняя граница, также противоречащее потребителям, гарантирует, что оно управляется A и всеми суперклассами, поэтому его можно безопасно добавлять.

List<? super A> listSuperA;
listSuperA = listA;
listSuperA = new ArrayList<Object>();

//add
listSuperA.add(new A());
listSuperA.add(new B());

//get
Object o0 = listSuperA.get(0);

Решение № 2

<? extends A>ака верхняя граница ака ковариантность ака производителей гарантирует, что он работает по A и всем подклассам, поэтому его безопасно получать и разыгрывать

List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;

//get
A a0 = listExtendsA.get(0);

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.