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

729

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

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

peakit
источник
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

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

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

anoopelias
источник
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.
        */  
    }
}

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

Premraj
источник
Эй, я просто хотел узнать, что вы имели в виду в последнем предложении: «Если вы считаете, что моя аналогия неверна, пожалуйста, обновите». Вы имеете в виду, если это этически неправильно (это субъективно) или если это неправильно в контексте программирования (что объективно: нет, это не так)? Я хотел бы заменить его более нейтральным примером, который является универсально приемлемым, независимо от культурных норм и этических убеждений; Если это нормально с тобой.
Нейрон
наконец я мог получить это. Хорошее объяснение.
Олег Куц
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в качестве параметра типа общего возвращаемого типа.

ColinD
источник
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.

mouselabs
источник
2

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

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

Джейсон
источник
1

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

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

yoAlex5
источник