O que é o PECS (Producer Extends Consumer Super)?

729

Me deparei com o PECS (abreviação de Producer extendsand Consumersuper ) enquanto lia sobre genéricos.

Alguém pode me explicar como usar o PECS para resolver confusões entre extendse super?

pico
fonte
3
Uma explicação muito boa com um exemplo @ youtube.com/watch?v=34oiEq9nD0M&feature=youtu.be&t=1630 que explica superparte, mas dá uma idéia de outra.
Lupchiazoem

Respostas:

844

tl; dr: "PECS" é do ponto de vista da coleção. Se você está apenas puxando itens de uma coleção genérica, é um produtor e você deve usá-lo extends; se você estiver enchendo apenas itens, ele é um consumidor e você deve usá-lo super. Se você fizer ambos com a mesma coleção, você não deve usar extendsou super.


Suponha que você tenha um método que tome como parâmetro uma coleção de coisas, mas deseja que seja mais flexível do que apenas aceitar a Collection<Thing>.

Caso 1: você deseja percorrer a coleção e fazer as coisas com cada item.
Então a lista é um produtor , então você deve usar a Collection<? extends Thing>.

O raciocínio é que a Collection<? extends Thing>poderia conter qualquer subtipo de Thinge, portanto, cada elemento se comportará como Thingquando você executa sua operação. (Na verdade, você não pode adicionar nada a Collection<? extends Thing>, porque não pode saber em tempo de execução qual subtipo específico da Thingcoleção contém.)

Caso 2: você deseja adicionar itens à coleção.
Então a lista é um consumidor , então você deve usar a Collection<? super Thing>.

O raciocínio aqui é que Collection<? extends Thing>, diferentemente , Collection<? super Thing>sempre pode conter, Thingnão importa qual seja o tipo parametrizado real. Aqui você não se importa com o que já está na lista, desde que permita que um Thingseja adicionado; é isso que ? super Thinggarante.

Michael Myers
fonte
142
Estou sempre tentando pensar dessa maneira: um produtor pode produzir algo mais específico e, portanto , se estende , um consumidor pode aceitar algo mais geral e, portanto, super .
Feuermurmel
10
Outra maneira de lembrar a distinção produtor / consumidor é pensar em uma assinatura de método. Se você possui um método doSomethingWithList(List list), está consumindo a lista e, portanto, precisará de covariância / extensão (ou uma Lista invariável). Por outro lado, se o seu método for List doSomethingProvidingList, você estará produzindo a Lista e precisará de contravariância / super (ou uma Lista invariável).
Raman
3
@MichaelMyers: Por que não podemos simplesmente usar um tipo parametrizado para esses dois casos? Existe alguma vantagem específica no uso de curingas aqui, ou é apenas um meio de melhorar a legibilidade semelhante a, digamos, usar referências a constcomo parâmetros de método em C ++ para significar que o método não modifica os argumentos?
Chatterjee
7
@Raman, acho que você acabou de confundir. No doSthWithList (você pode ter a List <? Super Thing>), já que você é consumidor, pode usar o super (lembre-se, CS). No entanto, é a lista <? estende Thing> getList (), pois você pode retornar algo mais específico ao produzir (PE).
Masterxilo 27/05
4
@AZ_ Partilho o seu sentimento. Se um método obtiver () da lista, o método será considerado um Consumidor <T> e a lista será considerada um provedor; mas a regra do PECS é "do ponto de vista da lista", portanto é necessário "estender". Deve ser GEPS: get extends; colocar super.
Treefish Zhang
561

Os princípios por trás disso na ciência da computação são chamados

  • Covariance: ? extends MyClass,
  • Contravariância: ? super MyClasse
  • Invariância / não variação: MyClass

A figura abaixo deve explicar o conceito. Imagem cedida por: Andrey Tyukin

Covariância vs Contravariância

anoopelias
fonte
144
Ei pessoal. Sou Andrey Tyukin, só queria confirmar que a anoopelias e a DaoWen entraram em contato comigo e obtiveram minha permissão para usar o esboço. Ele está licenciado sob (CC) -BY-SA. Thx @ Anoop por dar uma segunda vida ^^ @Brian Agnew: (em "poucos votos"): Isso é porque é um esboço para o Scala, ele usa a sintaxe do Scala e assume a variação do site de declaração, que é bem diferente da chamada estranha de Java variância -site ... Talvez eu devesse escrever uma resposta mais detalhada que mostra claramente como este esboço se aplica Java ...
Andrey Tyukin
3
Esta é uma das explicações mais simples e claras para covariância e contravariância que eu já encontrei!
Cs4r #
@Andrey Tyukin Oi, eu também quero usar esta imagem. Como posso entrar em contato com você?
slouc
Se você tiver alguma dúvida sobre esta ilustração, podemos discuti-la na sala de bate-papo: chat.stackoverflow.com/rooms/145734/…
Andrey Tyukin
49

PECS (Produtor extendse Consumidor super)

mnemônico → princípio Get and Put.

Este princípio afirma que:

  • Use um curinga estendido quando você obtém apenas valores de uma estrutura.
  • Use um super curinga quando você coloca apenas valores em uma estrutura.
  • E não use um curinga quando você recebe e coloca.

Exemplo em 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

}

Princípio de substituição de Liskov: se S é um subtipo de T, objetos do tipo T podem ser substituídos por objetos do tipo S.

No sistema de tipos de uma linguagem de programação, uma regra de digitação

  • covariante se preservar a ordem dos tipos (≤), que ordena os tipos de mais específico para mais genérico;
  • contravariante se reverter essa ordem;
  • invariável ou não variável, se nada disso se aplicar.

Covariância e contravariância

  • Tipos de dados somente leitura (fontes) podem ser covariantes ;
  • tipos de dados somente para gravação (coletores) podem ser contrários .
  • Tipos de dados mutáveis ​​que atuam como fontes e sumidouros devem ser invariáveis .

Para ilustrar esse fenômeno geral, considere o tipo de matriz. Para o tipo Animal, podemos criar o tipo Animal []

  • covariante : um gato [] é um animal [];
  • contravariante : um animal [] é um gato [];
  • invariante : um Animal [] não é um Gato [] e um Gato [] não é um Animal [].

Exemplos 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  

mais exemplos

curinga limitado (ou seja, indo para algum lugar) : Existem 3 tipos diferentes de curingas:

  • Variação / não variação: ?ou ? extends Object- Curinga não vinculado . Representa a família de todos os tipos. Use quando você pegar e colocar.
  • Co-variância: ? extends T(a família de todos os tipos que são subtipos de T) - um curinga com um limite superior . Té a classe mais alta na hierarquia de herança. Use um extendscuringa quando você apenas obtém valores de uma estrutura.
  • Contra-variância: ? super T(a família de todos os tipos que são supertipos de T) - um curinga com um limite inferior . Té a classe mais baixa na hierarquia de herança. Use um supercuringa quando você apenas coloca valores em uma estrutura.

Nota: curinga ?significa zero ou uma vez , representa um tipo desconhecido. O curinga pode ser usado como o tipo de um parâmetro, nunca usado como um argumento de tipo para uma chamada de método genérico, a criação da instância classe genérica. (Ou seja, quando curinga usado essa referência não utilizados em outras partes programa como usamos T)

insira a descrição da imagem aqui

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.
        */  
    }
}

genéricos e exemplos

Premraj
fonte
Ei, eu só queria saber o que você quis dizer com a última frase: "Se você acha que minha analogia está errada, atualize". Você quer dizer se é eticamente errado (o que é subjetivo) ou se está errado no contexto da programação (o que é objetivo: não, não está errado)? Eu gostaria de substituí-lo por um exemplo mais neutro, universalmente aceitável, independente de normas culturais e crenças éticas; Se estiver tudo bem com você.
Neuron
finalmente consegui entender. Boa explicação.
Oleg Kuts
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.não consigo adicionar elemento à Lista <?> Ou Lista <? estende Object>, então não entendo por que pode ser Use when you both get and put.
LiuWenbin_NO.
1
@LiuWenbin_NO. - Essa parte da resposta é enganosa. ?- o "curinga ilimitado" - corresponde ao exato oposto da invariância. Consulte a seguinte documentação: docs.oracle.com/javase/tutorial/java/generics/…, que declara: No caso em que o código precisa acessar a variável como variável "in" e "out", faça Não use um curinga. (Eles estão usando "in" e "out" como sinônimo de "get" e "put"). Com exceção de nullvocê não pode adicionar a uma coleção parametrizada com ?.
mouselabs 16/04
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
    }
}
Entalhe
fonte
Portanto, "? Estende B" deve ser interpretado como "? Estende B". É algo que B se estende para incluir todas as super classes de B até Objeto, excluindo B em si. Obrigado pelo código!
Saurabh Patil
3
@SaurabhPatil n, ? extends Bmeio B e qualquer coisa que se estende B.
ASGs
24

Como eu explico em minha resposta a outra pergunta, PECS é um dispositivo mnemônico criado por Josh Bloch para ajudar a lembrar P roducer extends, C onsumer super.

Isso significa que, quando um tipo parametrizado sendo passado para um método produzirá instâncias de T(elas serão recuperadas de alguma forma), ? extends Tdeve ser usado, pois qualquer instância de uma subclasse de Ttambém é a T.

Quando um tipo parametrizado sendo passado para um método consumirá instâncias de T(elas serão passadas a ele para fazer alguma coisa), ? super Tdeve ser usado porque uma instância de Tpode legalmente ser passada para qualquer método que aceite algum supertipo de T. A Comparator<Number>poderia ser usado em um Collection<Integer>, por exemplo. ? extends Tnão funcionaria, porque a Comparator<Integer>não poderia operar em a Collection<Number>.

Observe que geralmente você só deve usar ? extends Te ? super Tpara os parâmetros de algum método. Os métodos devem usar apenas Tcomo o parâmetro type em um tipo de retorno genérico.

ColinD
fonte
1
Esse princípio vale apenas para coleções? Faz sentido quando se tenta correlacioná-lo com uma lista. Se você pensar na assinatura de classificação (Lista <T>, Comparador <? Super T>) ---> aqui o Comparador usa super, portanto significa que é um consumidor no contexto do PECS. Quando você olha para a implementação, por exemplo, como: public int compare (Pessoa a, Pessoa b) {return a.age <b.age? -1: a.age == b.age? 0: 1; } Sinto que Person não consome nada, mas produz idade. Isso me deixa confuso. Existe uma falha no meu raciocínio ou o PECS é válido apenas para cobranças?
Fatih Arslan
24

Em poucas palavras, três regras fáceis de lembrar do PECS:

  1. Use o <? extends T>curinga se precisar recuperar o objeto do tipo Tde uma coleção.
  2. Use o <? super T>curinga se precisar colocar objetos do tipo Tem uma coleção.
  3. Se você precisar satisfazer as duas coisas, bem, não use nenhum curinga. Tão simples como isso.
Pradeep Kr Kaushal
fonte
10

vamos assumir esta hierarquia:

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

Vamos esclarecer o PE - Producer Extends:

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

Por que você não pode adicionar objetos que estendem "Shark" nesta lista? gostar:

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

Como você possui uma lista que pode ser do tipo A, B ou C em tempo de execução , não é possível adicionar nenhum objeto do tipo A, B ou C, pois pode acabar com uma combinação que não é permitida no java.
Na prática, o compilador pode realmente ver na hora da compilação que você adiciona um B:

sharks.add(new HammerShark());

... mas não há como saber se, em tempo de execução, seu B será um subtipo ou supertipo do tipo de lista. No tempo de execução, o tipo de lista pode ser qualquer um dos tipos A, B, C. Portanto, você não pode adicionar o HammerSkark (supertipo) em uma lista do DeadHammerShark, por exemplo.

* Você dirá: "OK, mas por que não consigo adicionar o HammerSkark, pois esse é o menor?". Resposta: É o menor que você conhece. Mas o HammerSkark também pode ser estendido por outra pessoa e você acaba no mesmo cenário.

Vamos esclarecer o CS - Consumer Super:

Na mesma hierarquia, podemos tentar o seguinte:

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

O que e por que você pode adicionar a esta lista?

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

Você pode adicionar os tipos de objetos acima, porque qualquer coisa abaixo do tubarão (A, B, C) sempre será subtipos de qualquer coisa acima do tubarão (X, Y, Z). Fácil de entender.

Você não pode adicionar tipos acima do Shark, porque em tempo de execução, o tipo de objeto adicionado pode ser mais alto na hierarquia que o tipo declarado da lista (X, Y, Z). Isso não é permitido.

Mas por que você não pode ler desta lista? (Quero dizer, você pode obter um elemento dele, mas não pode atribuí-lo a nada além de Objeto o):

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

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

Em tempo de execução, o tipo de lista pode ser qualquer tipo acima de A: X, Y, Z, ... O compilador pode compilar sua declaração de atribuição (o que parece correto), mas, em tempo de execução, o tipo de s (Animal) pode ser menor em hierarquia que o tipo declarado da lista (que pode ser Criatura ou superior). Isso não é permitido.

Resumindo

Usamos <? super T>para adicionar objetos de tipos iguais ou inferiores Tao List. Não podemos ler sobre isso.
Usamos <? extends T>para ler objetos de tipos iguais ou abaixo Tda lista. Não podemos adicionar elemento a ele.

Daniel
fonte
9

(adicionando uma resposta porque nunca há exemplos suficientes com curingas genéricos)

       // 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);
        }
    }
Andrejs
fonte
4

Esta é a maneira mais clara e simples de pensar em extensão versus super:

  • extendsé para leitura

  • superé para escrever

Eu acho que o "PECS" é uma maneira não óbvia de pensar sobre quem é o "produtor" e quem é o "consumidor". "PECS" é definido a partir da perspectiva da própria coleta de dados - a coleção "consome" se os objetos estão sendo escritas para ele (é consumir objetos de código de chamada), e "produz" se os objetos estão sendo lidos a partir dele (ele está produzindo objetos para algum código de chamada). Isso é contrário a como todo o resto é nomeado. As APIs Java padrão são nomeadas da perspectiva do código de chamada, não da coleção em si. Por exemplo, uma visão centrada na coleção de java.util.List deve ter um método chamado "receive ()" em vez de "add ()" - afinal,o elemento, mas a própria lista recebe o elemento.

Eu acho que é mais intuitivo, natural e consistente pensar nas coisas da perspectiva do código que interage com a coleção - o código "lê de" ou "escreve para" a coleção? Depois disso, qualquer código gravado na coleção seria o "produtor" e qualquer código lido na coleção seria o "consumidor".

Kaan
fonte
Encontrei a mesma colisão mental e tenderia a concordar, exceto que o PECS não especifica a nomeação do código e os limites de tipo são definidos nas declarações de coleção. Além disso, no que diz respeito à nomeação, muitas vezes você tem nomes para produzir / consumir coleções como srce dst. Então, você está lidando com código e contêineres ao mesmo tempo e acabei pensando sobre isso nesse sentido - "consumir código" consome de um contêiner de produção e "produzir código" produz para um contêiner de consumo.
mouselabs 17/04
4

A "regra" do PECS apenas garante que o seguinte seja legal:

  • Consumidor: seja o que ?for, pode se referir legalmente a T
  • Produtor: seja o que ?for, pode ser legalmente referido por T

O emparelhamento típico ao longo das linhas de List<? extends T> producer, List<? super T> consumeré simplesmente garantir que o compilador possa impor as regras padrão de relacionamento de herança "IS-A". Se pudéssemos fazer isso legalmente, seria mais simples dizer <T extends ?>, <? extends T>(ou melhor ainda, em Scala, como você pode ver acima) [-T], [+T]. Infelizmente, o melhor que podemos fazer é <? super T>, <? extends T>.

Quando o encontrei pela primeira vez e quebrei na cabeça, a mecânica fazia sentido, mas o código em si continuava confuso para mim - fiquei pensando "parece que os limites não precisam ser invertidos assim" - mesmo que eu ficou claro o que foi dito acima - que se trata simplesmente de garantir a conformidade com as regras padrão de referência.

O que me ajudou foi vê-lo usando a atribuição comum como analogia.

Considere o seguinte código de brinquedo (não pronto para produção):

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

Ilustrando isso em termos da analogia atribuição, para consumero ?wildcard (tipo desconhecido) é a referência - o "lado esquerdo" do trabalho - e <? super T>garante que tudo o que ?é, T"é-A" ?- que Tpode ser atribuído a ele, porque ?é um super tipo (ou, no máximo, o mesmo tipo) de T.

Para producera preocupação é o mesmo que seja apenas invertido: producer's ?curinga (tipo desconhecido) é o referente - o 'lado direito' do trabalho - e <? extends T>garante que tudo o que ?é, ?'é-A' T- que ele pode ser atribuído a umT , porque ?é um subtipo (ou pelo menos o mesmo tipo) de T.

mouselabs
fonte
2

Lembre-se disso:

Consumidor janta (super); Produtor amplia fábrica de seus pais

Jason
fonte
1

Usando o exemplo da vida real (com algumas simplificações):

  1. Imagine um trem de carga com vagões como analogia a uma lista.
  2. Você pode colocar uma carga em um vagão de carga se a carga tiver o mesmo ou menor tamanho que o vagão de carga =<? super FreightCarSize>
  3. Você pode descarregar uma carga de um vagão se tiver espaço suficiente (mais do que o tamanho da carga) em seu depósito =<? extends DepotSize>
contrapost
fonte
1

Covariância : aceitar subtipos
Contravariância : aceitar supertipos

Tipos covariantes são somente leitura, enquanto tipos contravariantes são somente gravação.

Farrukh Chishti
fonte
0

Vamos dar uma olhada no exemplo

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

Genéricos permite que você trabalhe com tipos dinamicamente de uma maneira segura

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

Problema

Como o Java's Collection é um tipo de referência, temos os próximos problemas:

Problema # 1

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

* O genérico do Swift não tem esse problema porque Collection é Value type[About], portanto, uma nova coleção é criada

Problema # 2

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

A solução - Curingas genéricos

Curinga é um recurso do tipo de referência e não pode ser instanciado diretamente

A solução 1, <? super A> conhecida como contravariância de limite inferior, ou consumidores, garante que é operada por A e por todas as superclasses, por isso é seguro adicionar

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

Solução # 2

<? extends A>aka limite superior aka covariância aka produtores garante que é operado por A e por todas as subclasses, é por isso que é seguro obter e transmitir

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

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

yoAlex5
fonte