Jestem trochę zdezorientowany, jak generycy Java radzą sobie z dziedziczeniem / polimorfizmem.
Załóżmy następującą hierarchię -
Animal (Parent)
Pies - Kot (Dzieci)
Załóżmy, że mam metodę doSomething(List<Animal> animals)
. Przez wszystkich zasad dziedziczenia i polimorfizmu, chciałbym założyć, że List<Dog>
jestList<Animal>
i List<Cat>
jestList<Animal>
- a więc albo można być przekazywane do tej metody. Skąd. Jeśli chcę osiągnąć takie zachowanie, muszę wyraźnie powiedzieć metodzie, aby zaakceptowała listę dowolnej podklasy Zwierząt, mówiąc doSomething(List<? extends Animal> animals)
.
Rozumiem, że takie jest zachowanie Javy. Moje pytanie brzmi: dlaczego ? Dlaczego polimorfizm jest na ogół domniemany, ale jeśli chodzi o leki generyczne, należy je określić?
źródło
Odpowiedzi:
Nie,
List<Dog>
to nieList<Animal>
. Zastanów się, co możesz zrobić zList<Animal>
- możesz do niego dodać dowolne zwierzę ... w tym kota. Czy możesz logicznie dodać kota do miotu szczeniąt? Absolutnie nie.Nagle masz bardzo zdezorientowanego kota.
Teraz nie możesz dodać
Cat
do,List<? extends Animal>
ponieważ nie wiesz, że toList<Cat>
. Możesz odzyskać wartość i wiedzieć, że będzieAnimal
, ale nie możesz dodawać dowolnych zwierząt. Odwrotnie jestList<? super Animal>
w przypadku - w takim przypadku możeszAnimal
bezpiecznie do niego dodać , ale nie wiesz nic o tym, co można z niego odzyskać, ponieważ może to byćList<Object>
.źródło
To, czego szukasz, nazywa się parametrami typu kowariantnego . Oznacza to, że jeśli jeden typ obiektu można zastąpić innym w metodzie (na przykład
Animal
można go zastąpićDog
), to samo dotyczy wyrażeń używających tych obiektów (więcList<Animal>
można je zastąpićList<Dog>
). Problem polega na tym, że kowariancja nie jest ogólnie bezpieczna dla list zmiennych. Załóżmy, że maszList<Dog>
, i jest używany jakoList<Animal>
. Co się stanie, gdy spróbujesz dodać do tego kota,List<Animal>
który jest naprawdęList<Dog>
? Automatyczne zezwolenie na współzmienne parametrów typu powoduje uszkodzenie systemu typów.Przydałoby się dodać składnię, aby umożliwić określenie parametrów typu jako kowariantnych, co pozwala uniknąć
? extends Foo
deklaracji metod, ale powoduje dodatkową złożoność.źródło
Powodem a
List<Dog>
nie jest toList<Animal>
, że na przykład możesz wstawić aCat
doList<Animal>
, ale nie doList<Dog>
... możesz użyć symboli wieloznacznych, aby w miarę możliwości generyczne były bardziej rozszerzalne; na przykład czytanie z aList<Dog>
jest podobne do czytania zList<Animal>
- ale nie pisania.Informacje ogólne w języku Java oraz sekcja Ogólne informacje z samouczków Java zawierają bardzo dobre, dogłębne wyjaśnienie, dlaczego niektóre rzeczy są lub nie są polimorficzne lub dozwolone w przypadku leków ogólnych.
źródło
Myślę, że należy wspomnieć o tym, o czym wspominają inne odpowiedzi, że w tym czasie
prawdą jest również to
Sposób, w jaki działa intuicja OP - co oczywiście jest całkowicie aktualne - to drugie zdanie. Jeśli jednak zastosujemy tę intuicję, otrzymamy język, który nie jest typowy dla języka Java w swoim systemie typów: Załóżmy, że nasz język pozwala na dodanie kota do naszej listy psów. Co to by znaczyło? Oznaczałoby to, że lista przestaje być listą psów i pozostaje jedynie listą zwierząt. I lista ssaków i lista czworokątów.
Innymi słowy:
List<Dog>
w języku Java nie oznacza „listy psów” w języku angielskim, oznacza „listę, która może mieć psy i nic więcej”.Mówiąc bardziej ogólnie, intuicja OP nadaje się do języka, w którym operacje na obiektach mogą zmieniać ich typ , a raczej typ (typy) obiektu jest (dynamiczną) funkcją jego wartości.
źródło
Powiedziałbym, że sednem Generics jest to, że na to nie pozwala. Rozważ sytuację z tablicami, które pozwalają na tego rodzaju kowariancję:
Ten kod kompiluje się dobrze, ale generuje błąd czasu wykonywania (
java.lang.ArrayStoreException: java.lang.Boolean
w drugim wierszu). Nie jest bezpieczny. Celem Generics jest dodanie bezpieczeństwa typu kompilacji w czasie, w przeciwnym razie możesz po prostu trzymać się zwykłej klasy bez generics.Teraz są czasy, gdzie trzeba być bardziej elastycznym i to, co
? super Class
i? extends Class
służą. Pierwsze z nich ma miejsce, gdy trzeba wstawić do typuCollection
(na przykład), a drugie, gdy trzeba z niego czytać, w sposób bezpieczny dla typu. Ale jedynym sposobem na zrobienie obu naraz jest posiadanie określonego typu.źródło
Aby zrozumieć problem, warto porównać tablice.
List<Dog>
nie jest podklasąList<Animal>
.Ale
Dog[]
jest podklasąAnimal[]
.Tablice są powtarzalne i kowariantne .
Reifiable oznacza, że informacje o ich typie są w pełni dostępne w czasie wykonywania.
Dlatego tablice zapewniają bezpieczeństwo typu wykonawczego, ale nie bezpieczeństwo typu kompilacji.
I odwrotnie, w przypadku generycznych:
Generyczne są usuwane i niezmienne .
Dlatego generyczne nie mogą zapewnić bezpieczeństwa typu środowiska wykonawczego, ale zapewniają bezpieczeństwo typu kompilacji.
W poniższym kodzie, jeśli zmienne ogólne byłyby kowariantne, możliwe będzie zanieczyszczenie hałdy na linii 3.
źródło
Odpowiedzi tutaj podane nie do końca mnie przekonały. Zamiast tego robię inny przykład.
brzmi dobrze, prawda? Ale można przekazać tylko
Consumer
S iSupplier
S naAnimal
s. Jeśli maszMammal
konsumenta, aleDuck
dostawcę, nie powinien on pasować, chociaż oba są zwierzętami. Aby temu zapobiec, dodano dodatkowe ograniczenia.Zamiast powyższego musimy zdefiniować relacje między używanymi typami.
Np.
dba o to, abyśmy mogli korzystać tylko z usług dostawcy, który zapewnia nam odpowiedni rodzaj przedmiotu dla konsumenta.
OTOH, równie dobrze możemy to zrobić
gdzie idziemy w drugą stronę: definiujemy typ
Supplier
i ograniczamy możliwość umieszczenia go w plikuConsumer
.Możemy nawet zrobić
gdzie, posiadający intuicyjne relacje
Life
->Animal
->Mammal
->Dog
,Cat
itd., możemy nawet umieścićMammal
naLife
konsumenta, ale nieString
doLife
konsumenta.źródło
(Consumer<Runnable>, Supplier<Dog>)
dopókiDog
jest podtypAnimal & Runnable
Podstawową logiką takiego zachowania jest
Generics
mechanizm usuwania typu. Tak więc w czasie wykonywania nie ma sposobu, aby zidentyfikować typ, wcollection
przeciwieństwie do tego,arrays
gdzie nie ma takiego procesu usuwania. Wracając do pytania ...Załóżmy, że istnieje metoda podana poniżej:
Teraz, jeśli java pozwala dzwoniącemu na dodanie listy typu Animal do tej metody, możesz dodać niewłaściwą rzecz do kolekcji, a także w czasie wykonywania będzie działać z powodu usunięcia typu. Podczas gdy w przypadku tablic otrzymasz wyjątek czasu wykonywania dla takich scenariuszy ...
Zasadniczo zachowanie to jest realizowane w taki sposób, że nie można dodać niewłaściwej rzeczy do kolekcji. Teraz uważam, że istnieje możliwość usunięcia typu, aby zapewnić zgodność ze starszą wersją języka Java bez generycznych ...
źródło
Podtypowanie jest niezmienne dla sparametryzowanych typów. Mimo że klasa
Dog
jest podtypemAnimal
, sparametryzowany typList<Dog>
nie jest podtypemList<Animal>
. W przeciwieństwie do tego, tablice wykorzystują kowariantne podtypy, więc typ tablicyDog[]
jest podtypemAnimal[]
.Niezmienne podtypowanie zapewnia, że ograniczenia typu wymuszone przez Javę nie zostaną naruszone. Rozważ następujący kod podany przez @Jona Skeeta:
Jak stwierdził @Jon Skeet, ten kod jest nielegalny, ponieważ w przeciwnym razie naruszyłby ograniczenia typu, zwracając kota, gdy pies tego oczekiwał.
Pouczające jest porównanie powyższego z analogicznym kodem dla tablic.
Kod jest legalny. Zgłasza jednak wyjątek magazynu tablic . Tablica przenosi swój typ w czasie wykonywania, w ten sposób JVM może wymusić bezpieczeństwo typu podtypu kowariantnego.
Aby to zrozumieć, spójrzmy na kod bajtowy wygenerowany przez
javap
poniższą klasę:Za pomocą polecenia
javap -c Demonstration
wyświetla się następujący kod bajtowy Java:Zauważ, że przetłumaczony kod treści metod jest identyczny. Kompilator zastąpił każdy sparametryzowany typ swoim skasowaniem . Ta właściwość ma kluczowe znaczenie, co oznacza, że nie złamała kompatybilności wstecznej.
Podsumowując, bezpieczeństwo w czasie wykonywania nie jest możliwe dla sparametryzowanych typów, ponieważ kompilator zastępuje każdy sparametryzowany typ przez jego usunięcie. To sprawia, że sparametryzowane typy są niczym więcej niż cukrem syntaktycznym.
źródło
W rzeczywistości możesz użyć interfejsu, aby osiągnąć to, co chcesz.
}
możesz następnie użyć kolekcji za pomocą
źródło
Jeśli masz pewność, że elementy listy są podklasami danego podtypu, możesz rzucić listę przy użyciu tego podejścia:
Jest to przydatne, gdy chcesz przekazać listę w konstruktorze lub iterować nad nią
źródło
odpowiedź , jak również inne odpowiedzi są poprawne. Dodam do tych odpowiedzi rozwiązanie, które moim zdaniem będzie pomocne. Myślę, że często pojawia się to w programowaniu. Należy zauważyć, że w przypadku kolekcji (list, zestawów itp.) Głównym problemem jest dodawanie do kolekcji. Tam rzeczy się psują. Nawet usunięcie jest w porządku.
W większości przypadków możemy użyć
Collection<? extends T>
raczej wtedyCollection<T>
i to powinien być pierwszy wybór. Znajduję jednak przypadki, w których nie jest to łatwe. To zależy od tego, czy zawsze jest to najlepsza rzecz do zrobienia. Prezentuję tutaj klasę DownCastCollection, która może przekonwertować aCollection<? extends T>
naCollection<T>
(możemy zdefiniować podobne klasy dla List, Set, NavigableSet, ..), które będą używane podczas korzystania ze standardowego podejścia jest bardzo niewygodne. Poniżej znajduje się przykład tego, jak go używać (moglibyśmy również użyćCollection<? extends Object>
w tym przypadku, ale w prosty sposób zilustruję go za pomocą DownCastCollection.Teraz klasa:
}
źródło
Collections.unmodifiableCollection
Collection<? extends E>
już obsługuje to zachowanie poprawnie, chyba że użyjesz go w sposób, który nie jest bezpieczny dla typu (np. rzutujesz go na coś innego). Jedyną korzyścią, jaką widzę, jest to, że po wywołaniuadd
operacji generuje wyjątek, nawet jeśli ją rzuciłeś.Weźmy przykład z samouczka JavaSE
Dlaczego lista psów (kółek) nie powinna być uważana za domyślną listę zwierząt (kształtów) z powodu tej sytuacji:
Zatem „architekci” Java mieli 2 opcje, które rozwiązują ten problem:
nie bierz pod uwagę, że podtyp jest domyślnie jego nadtypem i podaj błąd kompilacji, tak jak dzieje się to teraz
rozważ podtyp jako jego nadtyp i ogranicz przy kompilacji metodę „dodaj” (więc w metodzie drawAll, jeśli zostanie przekazana lista kręgów, podtyp kształtu, kompilator powinien to wykryć i ograniczyć ci błąd kompilacji do zrobienia że).
Z oczywistych względów wybrał pierwszy sposób.
źródło
Powinniśmy również wziąć pod uwagę, w jaki sposób kompilator zagraża klasom ogólnym: w „tworzy” instancję innego typu za każdym razem, gdy wypełniamy ogólne argumenty.
Tak więc mamy
ListOfAnimal
,ListOfDog
,ListOfCat
, itp, które są odrębne klasy, które w końcu jest „stworzone” przez kompilator, gdy określamy ogólne argumenty. I to jest płaska hierarchia (właściwieList
to wcale nie jest hierarchia).Kolejnym argumentem, dlaczego kowariancja nie ma sensu w przypadku klas ogólnych, jest fakt, że u podstawy wszystkie klasy są takie same - są
List
instancjami. SpecjalizacjaList
poprzez wypełnienie ogólnego argumentu nie rozszerza klasy, po prostu sprawia, że działa ona dla tego konkretnego ogólnego argumentu.źródło
Problem został dobrze zidentyfikowany. Ale jest rozwiązanie; make doSomething generic:
teraz możesz wywołać doSomething za pomocą List <Dog> lub List <Cat> lub List <Animal>.
źródło
innym rozwiązaniem jest zbudowanie nowej listy
źródło
W nawiązaniu do odpowiedzi Jona Skeeta, który używa tego przykładowego kodu:
Na najgłębszym poziomie problem polega na tym
dogs
ianimals
dziel się referencją. Oznacza to, że jednym ze sposobów na wykonanie tej pracy byłoby skopiowanie całej listy, co złamałoby równość referencji:Po zadzwonieniu
List<Animal> animals = new ArrayList<>(dogs);
nie można bezpośrednio przypisaćanimals
ani jednego,dogs
ani jednegocats
:dlatego nie można umieścić niewłaściwego podtypu
Animal
na liście, ponieważ nie ma niewłaściwego podtypu - żadnego obiektu podtypu? extends Animal
można do niego dodaćanimals
.Oczywiście zmienia to semantykę, ponieważ listy
animals
idogs
nie są już udostępniane, więc dodawanie do jednej listy nie dodaje się do drugiej (co jest dokładnie tym, czego chcesz, aby uniknąć problemu, żeCat
można dodać do listy, która jest tylko powinien zawieraćDog
obiekty). Również skopiowanie całej listy może być nieefektywne. Rozwiązuje to jednak problem równoważności typów, przerywając równość odniesienia.źródło