Pierwsza mówi, że jest to „jakiś typ, który jest przodkiem E”; druga mówi, że jest to „jakiś typ, który jest podklasą E”. (W obu przypadkach samo E jest w porządku.)
Więc konstruktor używa ? extends E
formularza, więc gwarantuje, że kiedy pobierze wartości z kolekcji, wszystkie będą E lub jakąś podklasą (tj. Jest zgodna). drainTo
Metoda stara się umieścić wartości w kolekcji, więc kolekcja musi mieć typ elementu z E
lub superklasę .
Na przykład załóżmy, że masz taką hierarchię klas:
Parent extends Object
Child extends Parent
i a LinkedBlockingQueue<Parent>
. Możesz skonstruować to przekazanie w a, List<Child>
które bezpiecznie skopiuje wszystkie elementy, ponieważ każdy Child
jest rodzicem. Nie możesz przekazać a, List<Object>
ponieważ niektóre elementy mogą nie być zgodne z Parent
.
Podobnie możesz spuścić tę kolejkę do a, List<Object>
ponieważ every Parent
jest Object
..., ale nie możesz go spuścić do a, List<Child>
ponieważ List<Child>
oczekuje, że wszystkie jego elementy będą zgodne z Child
.
? extends InputStream
lub? super InputStream
możesz użyćInputStream
jako argumentu.Powody tego są oparte na tym, jak Java implementuje typy generyczne.
Przykład tablic
Dzięki tablicom możesz to zrobić (tablice są kowariantne)
Ale co by się stało, gdybyś spróbował to zrobić?
Ta ostatnia linia skompilowałaby się dobrze, ale jeśli uruchomisz ten kod, możesz uzyskać plik
ArrayStoreException
. Ponieważ próbujesz wstawić podwójną do tablicy liczb całkowitych (niezależnie od tego, czy uzyskasz dostęp przez odwołanie liczbowe).Oznacza to, że możesz oszukać kompilator, ale nie możesz oszukać systemu typów środowiska uruchomieniowego. Dzieje się tak, ponieważ tablice nazywamy typami reifiable . Oznacza to, że w czasie wykonywania Java wie, że ta tablica została faktycznie utworzona jako tablica liczb całkowitych, do której dostęp uzyskuje się przez odwołanie typu
Number[]
.Jak więc widać, jedną rzeczą jest rzeczywisty typ obiektu, a inną typ odniesienia, którego używasz, aby uzyskać do niego dostęp, prawda?
Problem z generycznymi językami Java
Problem z typami ogólnymi Java polega na tym, że kompilator odrzuca informacje o typie i nie są one dostępne w czasie wykonywania. Ten proces nazywa się wymazywaniem typu . Jest dobry powód do implementowania takich typów generycznych w Javie, ale to długa historia i musi to być związane, między innymi, z binarną zgodnością z wcześniej istniejącym kodem (zobacz Jak mamy dostępne typy generyczne ).
Ale ważne jest tutaj to, że ponieważ w czasie wykonywania nie ma informacji o typie, nie ma sposobu, aby upewnić się, że nie popełnimy zanieczyszczenia sterty.
Na przykład,
Jeśli kompilator Java nie powstrzyma Cię przed tym, system typów środowiska wykonawczego również nie może Cię powstrzymać, ponieważ w czasie wykonywania nie ma sposobu, aby określić, że ta lista miała być tylko listą liczb całkowitych. Środowisko wykonawcze Javy pozwoliłoby umieścić na tej liście cokolwiek zechcesz, kiedy powinna zawierać tylko liczby całkowite, ponieważ podczas tworzenia została zadeklarowana jako lista liczb całkowitych.
W związku z tym projektanci Java upewnili się, że nie można oszukać kompilatora. Jeśli nie możesz oszukać kompilatora (tak jak możemy to zrobić z tablicami), nie możesz również oszukać systemu typów wykonawczych.
W związku z tym mówimy, że typy generyczne nie podlegają ponownej wymianie .
Najwyraźniej utrudniłoby to polimorfizm. Rozważmy następujący przykład:
Teraz możesz go użyć w ten sposób:
Ale jeśli spróbujesz zaimplementować ten sam kod w kolekcjach ogólnych, nie odniesiesz sukcesu:
Otrzymasz błędy kompilatora, jeśli spróbujesz ...
Rozwiązaniem jest nauczenie się korzystania z dwóch potężnych funkcji generycznych języka Java, znanych jako kowariancja i kontrawariancja.
Kowariancja
Dzięki kowariancji możesz czytać elementy ze struktury, ale nie możesz niczego do niej zapisać. To wszystko są ważne deklaracje.
Możesz przeczytać
myNums
:Ponieważ możesz być pewien, że cokolwiek zawiera rzeczywista lista, można ją przesłać dalej do liczby (w końcu wszystko, co rozszerza liczbę, jest liczbą, prawda?)
Nie możesz jednak umieszczać niczego w kowariantnej strukturze.
Nie byłoby to dozwolone, ponieważ Java nie może zagwarantować, jaki jest rzeczywisty typ obiektu w strukturze ogólnej. Może to być wszystko, co rozszerza Number, ale kompilator nie może być tego pewien. Możesz więc czytać, ale nie pisać.
Sprzeczność
Z kontrawariancją możesz zrobić odwrotnie. Możesz umieścić rzeczy w ogólnej strukturze, ale nie możesz z niej odczytać.
W tym przypadku rzeczywistą naturą obiektu jest lista obiektów, a przez kontrawariancję możesz wstawić do niej liczby, zasadniczo dlatego, że wszystkie liczby mają wspólnego przodka obiektu. Jako takie, wszystkie liczby są obiektami i dlatego jest to poprawne.
Jednak nie możesz bezpiecznie odczytać niczego z tej kontrawariantnej struktury, zakładając, że otrzymasz liczbę.
Jak widać, gdyby kompilator zezwolił na napisanie tej linii, w czasie wykonywania wystąpiłby wyjątek ClassCastException.
Zasada Get / Put
W związku z tym użyj kowariancji, gdy zamierzasz tylko wyprowadzić wartości ogólne ze struktury, użyj kontrawariancji, gdy zamierzasz umieścić tylko wartości ogólne w strukturze i użyj dokładnego typu ogólnego, jeśli zamierzasz zrobić jedno i drugie.
Najlepszym przykładem, jaki mam, jest następujący, który kopiuje wszelkiego rodzaju liczby z jednej listy do innej listy. To tylko staje się przedmioty od źródła, a jedynie stawia pozycji w cel.
Dzięki możliwościom kowariancji i kontrawariancji działa to w takim przypadku:
źródło
List<Object> myObjs = new List<Object();
(co brakuje zamknięcia>
na sekundęObject
).super.methodName
. Podczas używania<? super E>
oznacza „coś wsuper
kierunku” w przeciwieństwie do czegoś wextends
kierunku. Przykład:Object
jest wsuper
kierunkuNumber
(ponieważ jest to superklasa) iInteger
jest wextends
kierunku (ponieważ się rozciągaNumber
).<? extends E>
definiujeE
jako górną granicę: „To może być rzutowane naE
”.<? super E>
definiujeE
jako dolną granicę: „E
można do tego rzutować”.źródło
Object
jest z natury klasą niższego poziomu, mimo że jest pozycją jako ostateczną superklasę (i jest rysowana pionowo w UML lub podobnych drzewach dziedziczenia). Nigdy nie byłem w stanie tego cofnąć, pomimo eonów prób.Spróbuję odpowiedzieć na to pytanie. Ale aby uzyskać naprawdę dobrą odpowiedź, powinieneś zajrzeć do książki Joshua Blocha Effective Java (2nd Edition). Opisuje mnemonik PECS, który oznacza „Producer Extends, Consumer Super”.
Chodzi o to, że jeśli kod pobiera wartości ogólne z obiektu, powinieneś użyć extends. ale jeśli tworzysz nowe wartości dla typu ogólnego, powinieneś użyć super.
Na przykład:
I
Ale naprawdę powinieneś sprawdzić tę książkę: http://java.sun.com/docs/books/effective/
źródło
<? super E>
znaczyany object including E that is parent of E
<? extends E>
znaczyany object including E that is child of E .
źródło
Możesz wyszukać w Google terminy kontrawariancja (
<? super E>
) i kowariancja (<? extends E>
). Odkryłem, że najbardziej użyteczną rzeczą podczas rozumienia typów ogólnych jest zrozumienie sygnatury metodyCollection.addAll
:Tak jak chciałbyś móc dodać a
String
doList<Object>
:Powinieneś także móc dodać
List<String>
(lub dowolną kolekcjęString
) za pomocąaddAll
metody:Należy jednak zdać sobie sprawę, że a
List<Object>
i aList<String>
nie są równoważne, a drugie nie jest podklasą pierwszego. Potrzebne jest pojęcie kowariantnego parametru typu - czyli<? extends T>
bitu.Gdy już to zrobisz, łatwo jest wymyślić scenariusze, w których chcesz również kontrawariancji (sprawdź
Comparable
interfejs).źródło
Przed odpowiedzią; Jasne, proszę
Przykład:
Mam nadzieję, że pomoże ci to lepiej zrozumieć symbole wieloznaczne.
źródło
Symbol wieloznaczny z górną granicą wygląda jak „? Rozszerza typ” i oznacza rodzinę wszystkich typów, które są podtypami typu, przy czym typ jest uwzględniony. Typ jest nazywany górną granicą.
Symbol wieloznaczny z dolną granicą wygląda jak „? Super Type” i oznacza rodzinę wszystkich typów, które są nadtypami typu Type, przy czym typ Type jest uwzględniony. Typ nazywa się dolną granicą.
źródło
Masz klasę Parent i klasę Child dziedziczoną z klasy Parent.Klasa Parent jest dziedziczona z innej klasy o nazwie GrandParent Class, więc kolejność dziedziczenia to Dziadek> Rodzic> Dziecko. Teraz <? extends Parent> - akceptuje klasę rodzica lub klasę podrzędną <? super Parent> - akceptuje klasę Parent lub klasę GrandParent
źródło