Jaka jest koncepcja wymazywania w produktach generycznych w Javie?
Zasadniczo jest to sposób, w jaki typy generyczne są implementowane w Javie za pomocą sztuczek kompilatora. Skompilowany kod ogólny faktycznie używa po prostu java.lang.Object
wszędzie tam, o czym mówisz T
(lub jakiegoś innego parametru typu) - i istnieją pewne metadane, które mówią kompilatorowi, że naprawdę jest to typ ogólny.
Kiedy kompilujesz kod w oparciu o typ lub metodę generyczną, kompilator sprawdza, co naprawdę masz na myśli (tj. Jaki jest argument typu T
) i weryfikuje w czasie kompilacji , czy robisz dobrze, ale emitowany kod ponownie po prostu mówi jeśli chodzi o java.lang.Object
- kompilator generuje dodatkowe rzutowania w razie potrzeby. W czasie wykonywania a List<String>
i a List<Date>
są dokładnie takie same; dodatkowe informacje o typie zostały usunięte przez kompilator.
Porównaj to z, powiedzmy, C #, gdzie informacje są zachowywane w czasie wykonywania, umożliwiając kodowi zawarcie wyrażeń, takich jak typeof(T)
to, które jest równoważne T.class
- z wyjątkiem tego, że to drugie jest nieprawidłowe. (Należy pamiętać, że istnieją dalsze różnice między typami ogólnymi .NET i typami Java.) Wymazywanie typów jest źródłem wielu „nieparzystych” komunikatów ostrzegawczych / błędów dotyczących typów ogólnych języka Java.
Inne zasoby:
Object
(w scenariuszu słabo wpisanym) jestList<String>
na przykład a ). W Javie jest to po prostu niewykonalne - możesz dowiedzieć się, że jest toArrayList
typ, ale nie jaki był oryginalny typ ogólny. Takie sytuacje mogą wystąpić na przykład w sytuacjach serializacji / deserializacji. Innym przykładem jest sytuacja, w której kontener musi być w stanie konstruować instancje swojego typu ogólnego - musisz przekazać ten typ osobno w Javie (asClass<T>
).Class<T>
parametr do konstruktora (lub metody ogólnej) po prostu dlatego, że Java nie zachowuje tych informacji. Spójrz naEnumSet.allOf
przykład - argument typu generycznego metody powinien wystarczyć; dlaczego muszę także określać „normalny” argument? Odpowiedź: wymazywanie typu. Takie rzeczy zanieczyszczają API. Interesuje Cię, czy często korzystałeś z generycznych .NET? (ciąg dalszy)Na marginesie, ciekawym ćwiczeniem jest faktyczne sprawdzenie, co robi kompilator podczas wymazywania - sprawia, że cała koncepcja jest nieco łatwiejsza do zrozumienia. Istnieje specjalna flaga, którą można przekazać kompilatorowi do wyjściowych plików java, które mają usunięte elementy generyczne i wstawione rzutowania. Przykład:
Jest
-printflat
to flaga przekazywana kompilatorowi, który generuje pliki. (-XD
Część jest tym, co mówi,javac
aby przekazać go do pliku wykonywalnego jar, który faktycznie kompiluje, a nie tylkojavac
, ale dygresuję ...) Jest-d output_dir
to konieczne, ponieważ kompilator potrzebuje miejsca na umieszczenie nowych plików .java.To oczywiście robi więcej niż tylko usuwanie; wszystkie automatyczne czynności, które wykonuje kompilator, są tutaj wykonywane. Na przykład, domyślne konstruktory są również wstawiane, nowe
for
pętle w stylu foreach są rozszerzane do zwykłychfor
pętli itp. Miło jest zobaczyć małe rzeczy, które dzieją się automagicznie.źródło
Erasure, dosłownie oznacza, że informacja o typie obecna w kodzie źródłowym jest usuwana ze skompilowanego kodu bajtowego. Zrozummy to za pomocą kodu.
Jeśli skompilujesz ten kod, a następnie zdekompilujesz go za pomocą dekompilatora Java, otrzymasz coś takiego. Zwróć uwagę, że zdekompilowany kod nie zawiera śladu informacji o typie obecnych w oryginalnym kodzie źródłowym.
źródło
jigawot
powiedzieć, to działa.Aby uzupełnić już bardzo kompletną odpowiedź Jona Skeeta, musisz zdać sobie sprawę, że koncepcja wymazywania typów wywodzi się z potrzeby zgodności z poprzednimi wersjami Javy .
Pierwotnie zaprezentowany na EclipseCon 2007 (już niedostępny), kompatybilność obejmowała następujące punkty:
Oryginalna odpowiedź:
W związku z tym:
Są propozycje większej reifikacji . Reify bycie „Uważaj abstrakcyjne pojęcie za rzeczywiste”, gdzie konstrukcje językowe powinny być pojęciami, a nie tylko cukrem syntaktycznym.
Powinienem również wspomnieć o
checkCollection
metodzie Java 6, która zwraca dynamicznie bezpieczny widok określonej kolekcji. Każda próba wstawienia elementu niewłaściwego typu spowoduje natychmiastowyClassCastException
.Mechanizm generyczny w języku zapewnia sprawdzanie typu (statycznego) w czasie kompilacji, ale możliwe jest pokonanie tego mechanizmu za pomocą niezaznaczonych rzutów .
Zwykle nie stanowi to problemu, ponieważ kompilator wyświetla ostrzeżenia o wszystkich takich niezaznaczonych operacjach.
Istnieją jednak sytuacje, w których samo sprawdzanie typu statycznego nie jest wystarczające, na przykład:
ClassCastException
wskazuje, że niepoprawnie wpisany element został umieszczony w sparametryzowanej kolekcji. Niestety wyjątek może wystąpić w dowolnym momencie po wstawieniu błędnego elementu, więc zazwyczaj dostarcza on niewiele lub nie dostarcza żadnych informacji o rzeczywistym źródle problemu.Aktualizacja lipiec 2012, prawie cztery lata później:
Obecnie (2012) jest to szczegółowo opisane w „ Regułach zgodności migracji interfejsu API (test podpisu) ”
źródło
Uzupełniając już uzupełnioną odpowiedź Jona Skeeta ...
Wspomniano, że wdrażanie leków generycznych poprzez usuwanie prowadzi do pewnych irytujących ograniczeń (np. Nie
new T[42]
). Wspomniano również, że głównym powodem robienia rzeczy w ten sposób była wsteczna kompatybilność w kodzie bajtowym. Jest to również (w większości) prawda. Wygenerowany kod bajtowy -target 1.5 różni się nieco od po prostu pozbawionego cukrów rzutowania -target 1.4. Z technicznego punktu widzenia możliwe jest nawet (dzięki ogromnej sztuczce) uzyskanie dostępu do wystąpień typów ogólnych w czasie wykonywania , udowadniając, że naprawdę jest coś w kodzie bajtowym.Bardziej interesującą kwestią (która nie została poruszona) jest to, że implementacja typów generycznych za pomocą wymazywania zapewnia nieco większą elastyczność w tym, co może osiągnąć system typów wysokiego poziomu. Dobrym przykładem może być implementacja JVM Scali w porównaniu z CLR. W JVM można bezpośrednio zaimplementować wyższe typy, ponieważ sama JVM nie nakłada żadnych ograniczeń na typy generyczne (ponieważ te „typy” są faktycznie nieobecne). Kontrastuje to z CLR, który ma wiedzę w czasie wykonywania o wystąpieniach parametrów. Z tego powodu samo środowisko CLR musi mieć jakąś koncepcję, w jaki sposób należy używać rodzajów generycznych, unieważniając próby rozszerzenia systemu o nieoczekiwane reguły. W rezultacie wyższe typy Scali w CLR są implementowane przy użyciu dziwnej formy wymazywania emulowanej w samym kompilatorze,
Wymazywanie może być niewygodne, gdy chcesz robić niegrzeczne rzeczy w czasie wykonywania, ale zapewnia największą elastyczność twórcom kompilatorów. Domyślam się, że po części dlatego to nie zniknie w najbliższym czasie.
źródło
Jak rozumiem (będąc facetem .NET ), JVM nie ma pojęcia generyków, więc kompilator zastępuje parametry typu Object i wykonuje całe rzutowanie za Ciebie.
Oznacza to, że typy generyczne Java są niczym innym jak cukrem składniowym i nie oferują żadnej poprawy wydajności dla typów wartości, które wymagają pakowania / rozpakowywania, gdy są przekazywane przez odniesienie.
źródło
Istnieją dobre wyjaśnienia. Dodam tylko przykład, aby pokazać, jak działa wymazywanie typu z dekompilatorem.
Oryginalna klasa,
Zdekompilowany kod z jego kodu bajtowego,
źródło
Dlaczego warto korzystać z Generices
Krótko mówiąc, typy generyczne umożliwiają określanie typów (klas i interfejsów) jako parametrów podczas definiowania klas, interfejsów i metod. Podobnie jak w przypadku bardziej znanych parametrów formalnych używanych w deklaracjach metod, parametry typu umożliwiają ponowne użycie tego samego kodu z różnymi danymi wejściowymi. Różnica polega na tym, że dane wejściowe do parametrów formalnych są wartościami, podczas gdy dane wejściowe do parametrów typu są typami. oda korzystająca z typów ogólnych ma wiele zalet w porównaniu z kodem nieogólnym:
Co to jest wymazywanie typów
Generics zostały wprowadzone do języka Java, aby zapewnić ściślejsze sprawdzanie typów w czasie kompilacji i wspierać programowanie ogólne. Aby zaimplementować typy ogólne, kompilator Java stosuje wymazywanie typów do:
[NB] -Co to jest metoda pomostowa? Krótko mówiąc, w przypadku sparametryzowanego interfejsu, takiego jak
Comparable<T>
, może to spowodować wstawienie dodatkowych metod przez kompilator; te dodatkowe metody nazywane są mostami.Jak działa wymazywanie
Usunięcie typu jest zdefiniowane w następujący sposób: usuń wszystkie parametry typu z typów sparametryzowanych i zamień dowolną zmienną typu na wymazanie jej powiązania lub na Object, jeśli nie ma ograniczenia, lub na wymazanie skrajnego lewego powiązania, jeśli ma wiele granic. Oto kilka przykładów:
List<Integer>
,List<String>
iList<List<String>>
jestList
.List<Integer>[]
jestList[]
.List
jest samo w sobie, podobnie dla każdego typu surowego.Integer
jest samo w sobie, podobnie dla każdego typu bez parametrów typu.T
w definicjiasList
jestObject
, ponieważT
nie ma granic.T
w definicjimax
jestComparable
, ponieważT
zostało ograniczoneComparable<? super T>
.T
w ostatecznej definicjimax
jestObject
, ponieważT
ma związaneObject
&,Comparable<T>
a my usuwamy skrajne lewe ograniczenie.Należy zachować ostrożność podczas korzystania z leków generycznych
W Javie dwie różne metody nie mogą mieć tego samego podpisu. Ponieważ typy generyczne są implementowane przez wymazywanie, wynika również, że dwie różne metody nie mogą mieć podpisów z tym samym wymazywaniem. Klasa nie może przeciążyć dwóch metod, których podpisy mają takie same wymazywanie, a klasa nie może implementować dwóch interfejsów, które mają to samo wymazywanie.
Zamierzamy ten kod działać w następujący sposób:
Jednak w tym przypadku wymazywanie podpisów obu metod jest identyczne:
Dlatego konflikt nazw jest zgłaszany w czasie kompilacji. Nie jest możliwe nadanie obu metodom tej samej nazwy i próba ich rozróżnienia przez przeciążenie, ponieważ po skasowaniu nie można odróżnić jednego wywołania metody od drugiego.
Miejmy nadzieję, że czytelnik się spodoba :)
źródło