Respostas:
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 extends
ou 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 Thing
e, portanto, cada elemento se comportará como Thing
quando 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 Thing
coleçã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, Thing
nã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 Thing
seja adicionado; é isso que ? super Thing
garante.
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).
const
como parâmetros de método em C ++ para significar que o método não modifica os argumentos?
Os princípios por trás disso na ciência da computação são chamados
? extends MyClass
,? super MyClass
eMyClass
A figura abaixo deve explicar o conceito. Imagem cedida por: Andrey Tyukin
PECS (Produtor extends
e Consumidor super
)
mnemônico → princípio Get and Put.
Este princípio afirma que:
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
Para ilustrar esse fenômeno geral, considere o tipo de matriz. Para o tipo Animal, podemos criar o tipo 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
curinga limitado (ou seja, indo para algum lugar) : Existem 3 tipos diferentes de curingas:
?
ou ? extends Object
- Curinga não vinculado . Representa a família de todos os tipos. Use quando você pegar e colocar.? 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 extends
curinga quando você apenas obtém valores de uma estrutura.? 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 super
curinga 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
)
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.
*/
}
}
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
.
?
- 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 null
você não pode adicionar a uma coleção parametrizada com ?
.
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
}
}
? extends B
meio B e qualquer coisa que se estende B.
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 T
deve ser usado, pois qualquer instância de uma subclasse deT
também é aT
.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 T
deve ser usado porque uma instância deT
pode legalmente ser passada para qualquer método que aceite algum supertipo deT
. AComparator<Number>
poderia ser usado em umCollection<Integer>
, por exemplo.? extends T
não funcionaria, porque aComparator<Integer>
não poderia operar em aCollection<Number>
.
Observe que geralmente você só deve usar ? extends T
e ? super T
para os parâmetros de algum método. Os métodos devem usar apenas T
como o parâmetro type em um tipo de retorno genérico.
Em poucas palavras, três regras fáceis de lembrar do PECS:
<? extends T>
curinga se precisar recuperar o objeto do tipo T
de uma coleção.<? super T>
curinga se precisar colocar objetos do tipo T
em uma coleção.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 T
ao List
. Não podemos ler sobre isso.
Usamos <? extends T>
para ler objetos de tipos iguais ou abaixo T
da lista. Não podemos adicionar elemento a ele.
(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);
}
}
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".
src
e 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.
A "regra" do PECS apenas garante que o seguinte seja legal:
?
for, pode se referir legalmente a T
?
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 consumer
o ?
wildcard (tipo desconhecido) é a referência - o "lado esquerdo" do trabalho - e <? super T>
garante que tudo o que ?
é, T
"é-A" ?
- que T
pode ser atribuído a ele, porque ?
é um super tipo (ou, no máximo, o mesmo tipo) de T
.
Para producer
a 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
.
Usando o exemplo da vida real (com algumas simplificações):
<? super FreightCarSize>
<? extends DepotSize>
Covariância : aceitar subtipos
Contravariância : aceitar supertipos
Tipos covariantes são somente leitura, enquanto tipos contravariantes são somente gravação.
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);
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;
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);
super
parte, mas dá uma idéia de outra.