Risposte:
tl; dr: "PECS" è dal punto di vista della collezione. Se stai solo estraendo oggetti da una collezione generica, è un produttore e dovresti usare extends
; se stai solo riempiendo oggetti, è un consumatore e dovresti usarlo super
. Se si fa sia con la stessa collezione, non si dovrebbe usare uno extends
o super
.
Supponiamo di avere un metodo che prende come parametro una raccolta di cose, ma che vuoi che sia più flessibile che accettare semplicemente un Collection<Thing>
.
Caso 1: vuoi passare attraverso la raccolta e fare le cose con ogni oggetto.
Quindi l'elenco è un produttore , quindi dovresti usare a Collection<? extends Thing>
.
Il ragionamento è che a Collection<? extends Thing>
potrebbe contenere qualsiasi sottotipo di Thing
, e quindi ogni elemento si comporterà come Thing
quando esegui la tua operazione. (In realtà non è possibile aggiungere nulla a a Collection<? extends Thing>
, perché non è possibile sapere in fase di esecuzione quale sottotipo specifico della Thing
raccolta contiene.)
Caso 2: vuoi aggiungere elementi alla raccolta.
Quindi l'elenco è un consumatore , quindi dovresti usare a Collection<? super Thing>
.
Il ragionamento qui è che a differenza di Collection<? extends Thing>
, Collection<? super Thing>
può sempre tenere una Thing
qualunque cosa sia il tipo con parametri effettivi. Qui non ti interessa ciò che è già nella lista purché permetta Thing
di aggiungere un; questo è ciò che ? super Thing
garantisce.
doSomethingWithList(List list)
, stai consumando l'elenco e quindi avrai bisogno di covarianza / estensione (o di un elenco invariante). D'altra parte, se il tuo metodo è List doSomethingProvidingList
, allora stai producendo la Lista e avrai bisogno di contraddizione / super (o di una Lista invariante).
const
come parametri di metodo in C ++ per indicare che il metodo non modifica gli argomenti?
I principi dietro questo in informatica sono chiamati
? extends MyClass
,? super MyClass
eMyClass
L'immagine sotto dovrebbe spiegare il concetto. Immagine gentilmente concessa : Andrey Tyukin
PECS (produttore extends
e consumatore super
)
mnemonico → Principio Get and Put.
Questo principio afferma che:
Esempio in 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
}
Principio di sostituzione di Liskov: se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti con oggetti di tipo S.
All'interno del sistema di tipi di un linguaggio di programmazione, una regola di battitura
Per illustrare questo fenomeno generale, considerare il tipo di array. Per il tipo Animale possiamo creare il tipo Animale []
Esempi 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
jolly delimitato (ovvero diretto verso qualche parte) : esistono 3 diversi tipi di jolly:
?
oppure ? extends Object
- Senza limiti jolly . Rappresenta la famiglia di tutti i tipi. Usalo quando ottieni e metti entrambi.? extends T
(la famiglia di tutti i tipi che sono sottotipi di T
) - un carattere jolly con un limite superiore . T
è la classe più in alto nella gerarchia dell'ereditarietà. Utilizzare un extends
carattere jolly solo quando si ottiene valori da una struttura.? super T
(la famiglia di tutti i tipi che sono supertipi di T
) - un carattere jolly con un limite inferiore . T
è la classe più bassa nella gerarchia dell'ereditarietà. Utilizzare un super
carattere jolly quando si inseriscono solo valori in una struttura.Nota: il carattere jolly ?
significa zero o una volta , rappresenta un tipo sconosciuto. Il carattere jolly può essere utilizzato come tipo di parametro, mai utilizzato come argomento di tipo per un richiamo di metodo generico, una creazione di istanza di classe generica (ovvero quando viene utilizzato un carattere jolly che non fa riferimento a nessun'altra parte del programma come noi 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.
Non riesco ad aggiungere elementi all'elenco <?> O all'elenco <? estende Object>, quindi non capisco perché possa essere Use when you both get and put
.
?
- il "jolly senza limiti" - corrisponde all'esatto contrario dell'invarianza. Fare riferimento alla seguente documentazione: docs.oracle.com/javase/tutorial/java/generics/… che indica: Nel caso in cui il codice debba accedere alla variabile come variabile "in" e "out", fare non utilizzare un carattere jolly. (Stanno usando "in" e "out" come sinonimo di "get" e "put"). Ad eccezione di null
te, non puoi aggiungerlo a una Collezione con parametri ?
.
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
significa B e qualsiasi cosa si estenda B.
Come spiego in mia risposta a un'altra domanda, PECS è un dispositivo mnemonico creato da Josh Bloch Per ricordare P roducer extends
, C CONSUMATORI super
.
Ciò significa che quando un tipo con parametri che viene passato a un metodo produrrà istanze di
T
(saranno recuperate da esso in qualche modo),? extends T
dovrebbe essere usato, poiché anche qualsiasi istanza di una sottoclasse diT
è aT
.Quando un tipo con parametri che viene passato a un metodo consumerà istanze di
T
(saranno passati ad esso per fare qualcosa),? super T
dovrebbe essere usato perché un'istanza diT
può essere legalmente passata a qualsiasi metodo che accetta un supertipo diT
. AComparator<Number>
potrebbe essere usato su unCollection<Integer>
, per esempio.? extends T
non funzionerebbe, perché aComparator<Integer>
non poteva operare su aCollection<Number>
.
Si noti che generalmente si dovrebbe usare solo ? extends T
e ? super T
per i parametri di alcuni metodi. I metodi dovrebbero usare solo T
come parametro type su un tipo restituito generico.
In poche parole, tre semplici regole da ricordare PECS:
<? extends T>
carattere jolly se è necessario recuperare un oggetto di tipo T
da una raccolta.<? super T>
carattere jolly se è necessario inserire oggetti di tipo T
in una raccolta.assumiamo questa gerarchia:
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
Chiariamo PE - Il produttore estende:
List<? extends Shark> sharks = new ArrayList<>();
Perché non puoi aggiungere oggetti che estendono "Shark" in questo elenco? piace:
sharks.add(new HammerShark());//will result in compilation error
Dato che hai un elenco che può essere di tipo A, B o C in fase di esecuzione , non puoi aggiungere alcun oggetto di tipo A, B o C perché puoi finire con una combinazione non consentita in Java.
In pratica, il compilatore può effettivamente vedere a compiletime che aggiungi una B:
sharks.add(new HammerShark());
... ma non ha modo di sapere se in fase di esecuzione, la tua B sarà un sottotipo o un supertipo del tipo di elenco. In fase di esecuzione il tipo di elenco può essere uno dei tipi A, B, C. Quindi non è possibile aggiungere HammerSkark (super tipo) in un elenco, ad esempio, di DeadHammerShark.
* Dirai: "OK, ma perché non posso aggiungere HammerSkark perché è il tipo più piccolo?". Risposta: è il più piccolo che conosci. Ma HammerSkark può essere esteso anche da qualcun altro e si finisce nello stesso scenario.
Chiariamo CS - Consumer Super:
Nella stessa gerarchia possiamo provare questo:
List<? super Shark> sharks = new ArrayList<>();
Cosa e perché puoi aggiungere a questo elenco?
sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());
Puoi aggiungere i tipi di oggetti sopra indicati perché qualsiasi cosa sotto lo squalo (A, B, C) sarà sempre sottotipo di qualsiasi cosa sopra lo squalo (X, Y, Z). Facile da capire.
Non è possibile aggiungere tipi sopra Shark, poiché in fase di runtime il tipo di oggetto aggiunto può essere superiore nella gerarchia rispetto al tipo dichiarato dell'elenco (X, Y, Z). Questo non è permesso
Ma perché non puoi leggere da questo elenco? (Voglio dire che puoi estrarre un elemento da esso, ma non puoi assegnarlo ad altro che all'Oggetto o):
Object o;
o = sharks.get(2);// only assignment that works
Animal s;
s = sharks.get(2);//doen't work
In fase di esecuzione, il tipo di elenco può essere di qualsiasi tipo sopra A: X, Y, Z, ... Il compilatore può compilare l'istruzione di assegnazione (che sembra corretta) ma, in fase di esecuzione, il tipo di s (Animale) può essere inferiore in gerarchia rispetto al tipo dichiarato dell'elenco (che potrebbe essere Creatura o superiore). Questo non è permesso
Per riassumere
Usiamo <? super T>
per aggiungere oggetti di tipo uguale o inferiore T
a List
. Non possiamo leggere da esso.
Usiamo <? extends T>
per leggere oggetti di tipo uguale o inferiore T
dalla lista. Non possiamo aggiungere elementi ad esso.
(aggiungendo una risposta perché mai abbastanza esempi con i caratteri jolly di 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);
}
}
Questo è il modo più chiaro e più semplice per me pensare di estensioni contro super:
extends
è per la lettura
super
è per scrivere
Trovo che "PECS" sia un modo non ovvio di pensare a cose riguardanti chi è il "produttore" e chi è il "consumatore". "PECS" è definito dal punto di vista della raccolta di dati stessa : la raccolta "consuma" se gli oggetti vengono scritti su di essa (sta consumando oggetti dal codice chiamante) e "produce" se gli oggetti vengono letti da essa (esso sta producendo oggetti con un codice chiamante). Questo è contrario a come tutto il resto è chiamato però. Le API Java standard sono denominate dal punto di vista del codice chiamante, non della raccolta stessa. Ad esempio, una vista incentrata sulla raccolta di java.util.List dovrebbe avere un metodo denominato "receive ()" anziché "add ()" - dopo tutto,riceve l'elemento.
Penso che sia più intuitivo, naturale e coerente pensare alle cose dal punto di vista del codice che interagisce con la raccolta - il codice "legge da" o "scrive" nella raccolta? Successivamente, qualsiasi codice che scrive nella collezione sarebbe il "produttore", e qualsiasi codice letto dalla collezione sarebbe il "consumatore".
src
e dst
. Quindi hai a che fare sia con il codice che con i contenitori allo stesso tempo e ho finito per pensarci su queste linee - il "consumo di codice" consuma da un contenitore di produzione e "la produzione di codice" produce per un contenitore di consumo.
La "regola" PECS garantisce solo che quanto segue sia legale:
?
sia, può legalmente fare riferimento T
?
sia, può essere legalmente indicato da T
L'accoppiamento tipico lungo le linee di List<? extends T> producer, List<? super T> consumer
assicura semplicemente che il compilatore possa applicare le regole standard di relazione ereditaria "IS-A". Se potessimo farlo legalmente, potrebbe essere più semplice dirlo <T extends ?>, <? extends T>
(o meglio ancora a Scala, come puoi vedere sopra, è [-T], [+T]
. Sfortunatamente il meglio che possiamo fare è <? super T>, <? extends T>
.
Quando l'ho incontrato per la prima volta e l'ho rotto nella mia testa, la meccanica aveva un senso, ma il codice stesso continuava a sembrare confuso per me - continuavo a pensare "sembra che i limiti non debbano essere invertiti in quel modo" - anche se era chiaro quanto sopra - che si tratta semplicemente di garantire il rispetto delle regole standard di riferimento.
Ciò che mi ha aiutato è stato guardarlo usando il compito ordinario come analogia.
Considera il seguente codice giocattolo (non pronto per la produzione):
// 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);
}
Illustrandolo in termini di analogia del compito, per consumer
il ?
carattere jolly (tipo sconosciuto) è il riferimento - il "lato sinistro" del compito - e <? super T>
garantisce che qualunque cosa ?
sia, T
"IS-A" ?
- che T
può essere assegnata a esso, perché ?
è un super tipo (o al massimo lo stesso tipo) di T
.
Poiché producer
la preoccupazione è la stessa, è appena capovolta: producer
il ?
carattere jolly (tipo sconosciuto) è il referente - il "lato destro" del compito - e <? extends T>
garantisce che qualunque cosa ?
sia,?
'is-a' T
- che si può essere assegnato a unT
, poiché ?
è un sottotipo (o almeno lo stesso tipo) di T
.
Usando l'esempio della vita reale (con alcune semplificazioni):
<? super FreightCarSize>
<? extends DepotSize>
Covarianza : accettare i sottotipi
Contravarianza : accettare i supertipi
I tipi covarianti sono di sola lettura, mentre i tipi contraddittori sono di sola scrittura.
Diamo un'occhiata ad esempio
public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }
Generics ti consente di lavorare con i tipi in modo dinamico in modo sicuro
//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);
Poiché Java's Collection è un tipo di riferimento, di conseguenza abbiamo i seguenti problemi:
Problema n. 1
//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;
* Il generico di Swift non ha questo problema perché Collection è Value type
[About], quindi viene creata una nuova collezione
Problema n. 2
//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;
Il carattere jolly è una funzione del tipo di riferimento e non può essere istanziato direttamente
Soluzione n. 1
<? super A>
aka limite inferiore noto anche come contraddizione aka consumatori garantisce che è gestito da A e da tutte le superclassi, ecco perché è sicuro aggiungere
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);
Soluzione n. 2
<? extends A>
aka limite superiore aka covarianza aka produttori garantisce che è gestito da A e tutte le sottoclassi, ecco perché è sicuro ottenere e trasmettere
List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;
//get
A a0 = listExtendsA.get(0);
super
parte, ma dà un'idea di un'altra.