Usuwanie typu generics Java: kiedy i co się dzieje?


238

Przeczytałem o usuwaniu typu Java na stronie Oracle .

Kiedy następuje usunięcie typu? W czasie kompilacji czy w czasie wykonywania? Kiedy klasa jest załadowana? Kiedy instancja klasy jest tworzona?

Wiele stron (w tym oficjalny samouczek wspomniany powyżej) twierdzi, że kasowanie typu występuje podczas kompilacji. Jeśli informacje o typie zostaną całkowicie usunięte w czasie kompilacji, w jaki sposób JDK sprawdza zgodność typu, gdy wywoływana jest metoda wykorzystująca ogólne, bez informacji o typie lub informacji o niewłaściwym typie?

Rozważmy następujący przykład: klasa Say Ama metody empty(Box<? extends Number> b). Kompilujemy A.javai otrzymujemy plik klasy A.class.

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

Teraz możemy utworzyć inną klasę B, która wywołuje metodę emptyz non-parametryzowanego argumentu typu (RAW) empty(new Box()). Jeśli kompilujemy B.javasię A.classw ścieżce klas, javac jest wystarczająco inteligentny, aby wywołać ostrzeżenie. Więc A.class ma jakieś informacje o typie przechowywanych w nim.

public class B {
    public static void invoke() {
        // java: unchecked method invocation:
        //  method empty in class A is applied to given types
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        // java: unchecked conversion
        //  required: Box<? extends java.lang.Number>
        //  found:    Box
        A.empty(new Box());
    }
}

Domyślam się, że kasowanie typu ma miejsce, gdy klasa jest ładowana, ale to tylko przypuszczenie. Kiedy to się stanie?



@afryingpan: Artykuł wymieniony w mojej odpowiedzi szczegółowo wyjaśnia, w jaki sposób i kiedy ma miejsce usunięcie typu. Wyjaśnia także, kiedy przechowywane są informacje o typie. Innymi słowy: zmienione generyczne są dostępne w Javie, wbrew powszechnemu przekonaniu. Zobacz: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Odpowiedzi:


240

Kasowanie typu ma zastosowanie w przypadku generycznych. W pliku klasy zdecydowanie są metadane pozwalające stwierdzić, czy metoda / typ jest ogólna, jakie są ograniczenia itp. Ale kiedy używane są ogólne, są one przekształcane w kontrole czasu kompilacji i rzutowania w czasie wykonywania. Więc ten kod:

List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);

jest wkompilowany w

List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);

W czasie wykonywania nie ma możliwości sprawdzenia, czy T=Stringdla obiektu listy - ta informacja zniknęła.

... ale List<T>sam interfejs wciąż reklamuje się jako ogólny.

EDYCJA: Dla wyjaśnienia, kompilator zachowuje informacje o zmiennej, która jest List<String>- ale nadal nie możesz tego znaleźć T=Stringdla samego obiektu listy.


6
Nie, nawet przy użyciu typu ogólnego mogą być dostępne metadane w czasie wykonywania. Zmienna lokalna nie jest dostępna przez Reflection, ale dla parametru metody zadeklarowanego jako „List <String> l” w czasie wykonywania będą dostępne metadane typu, dostępne poprzez API Reflection. Tak, „kasowanie typu” nie jest tak proste, jak wielu ludzi myśli ...
Rogério

4
@Rogerio: Gdy odpowiedziałem na twój komentarz, wydaje mi się, że mylicie się między możliwością uzyskania typu zmiennej a uzyskaniem typu obiektu . Sam obiekt nie zna argumentu typu, nawet jeśli pole go zna.
Jon Skeet

Oczywiście, patrząc na sam obiekt, nie możesz wiedzieć, że jest to List <String>. Ale obiekty nie pojawiają się znikąd. Są one tworzone lokalnie, przekazywane jako argument wywołania metody, zwracane jako wartość zwracana z wywołania metody lub odczytywane z pola jakiegoś obiektu ... We wszystkich tych przypadkach MOŻNA wiedzieć w czasie wykonywania, jaki jest typ ogólny, albo pośrednio lub za pomocą Java Reflection API.
Rogério

13
@Rogerio: Skąd jednak wiesz, skąd pochodzi ten obiekt? Jeśli masz parametr typu, List<? extends InputStream>skąd możesz wiedzieć, jaki to był typ, kiedy został utworzony? Nawet jeśli można dowiedzieć się typ pola odniesienia zostały zawarte w, dlaczego trzeba? Dlaczego powinieneś mieć możliwość uzyskania wszystkich pozostałych informacji o obiekcie w czasie wykonywania, ale nie jego argumentów typu ogólnego? Wygląda na to, że próbujesz wymazać czcionkę, aby była to drobiazg, który tak naprawdę nie wpływa na programistów - podczas gdy w niektórych przypadkach jest to bardzo poważny problem.
Jon Skeet

Ale wymazanie typu JEST drobną rzeczą, która tak naprawdę nie wpływa na programistów! Oczywiście, nie mogę mówić w imieniu innych, ale z MOJEGO doświadczenia nigdy nie było wielkiej sprawy. Faktycznie korzystam z informacji o typie środowiska wykonawczego przy projektowaniu mocking API Java (JMockit); jak na ironię, pozorowane interfejsy API .NET wydają się w mniejszym stopniu korzystać z ogólnego systemu typów dostępnego w języku C #.
Rogério

99

Kompilator odpowiada za zrozumienie Generics w czasie kompilacji. Kompilator jest także odpowiedzialny za odrzucenie tego „zrozumienia” klas ogólnych, w procesie, który nazywamy skasowaniem typu . Wszystko dzieje się w czasie kompilacji.

Uwaga: W przeciwieństwie do przekonań większości programistów Java, możliwe jest zachowanie informacji o czasie kompilacji i pobieranie tych informacji w czasie wykonywania, mimo że w bardzo ograniczony sposób. Innymi słowy: Java zapewnia zreifikowane generyki w bardzo ograniczony sposób .

Jeśli chodzi o usuwanie typu

Należy zauważyć, że, w czasie kompilacji, kompilator posiada pełną informację o typie dostępne, ale ta informacja jest celowo spadła w ogóle , gdy kod bajtowy jest generowany w procesie znanym jako typ skasowaniem . Odbywa się to w ten sposób ze względu na problemy ze zgodnością: Intencją projektantów języków było zapewnienie pełnej zgodności kodu źródłowego i pełnej zgodności kodu bajtowego między wersjami platformy. Jeśli zostałby zaimplementowany inaczej, musiałbyś ponownie skompilować starsze aplikacje podczas migracji do nowszych wersji platformy. W ten sposób wszystkie sygnatury metod są zachowywane (zgodność kodu źródłowego) i nie trzeba niczego rekompilować (zgodność binarna).

Odnośnie zweryfikowanych generycznych w Javie

Jeśli chcesz zachować informacje o czasie kompilacji, musisz zastosować anonimowe klasy. Chodzi o to: w bardzo szczególnym przypadku anonimowych klas możliwe jest uzyskanie pełnej informacji o czasie kompilacji w czasie wykonywania, co oznacza innymi słowy: zreifikowane generyczne. Oznacza to, że kompilator nie wyrzuca informacji o typie, gdy zaangażowane są anonimowe klasy; informacje te są przechowywane w wygenerowanym kodzie binarnym, a system wykonawczy pozwala na ich odzyskanie.

Napisałem artykuł na ten temat:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

Uwaga na temat techniki opisanej w powyższym artykule jest taka, że ​​dla większości programistów technika ta jest mało znana. Pomimo tego, że działa i działa dobrze, większość programistów czuje się zagubiona lub niewygodna w tej technice. Jeśli masz wspólną bazę kodów lub planujesz opublikować swój kod publicznie, nie polecam powyższej techniki. Z drugiej strony, jeśli jesteś jedynym użytkownikiem swojego kodu, możesz skorzystać z mocy, jaką daje Ci ta technika.

Przykładowy kod

W powyższym artykule znajdują się linki do przykładowego kodu.


1
@ will824: Znacząco poprawiłem odpowiedź i dodałem link do niektórych przypadków testowych. Na zdrowie :)
Richard Gomes,

1
W rzeczywistości nie utrzymywali zarówno zgodności binarnej, jak i źródłowej: oracle.com/technetwork/java/javase/compatibility-137462.html Gdzie mogę dowiedzieć się więcej o ich zamiarze? Dokumenty mówią, że używa usuwania typu, ale nie mówią dlaczego.
Dzmitry Lazerka

@Richard Rzeczywiście, doskonały artykuł! Można dodać, że klasy lokalne też działają i że w obu przypadkach (klasy anonimowe i lokalne) informacje o żądanym typie argumentu są przechowywane tylko w przypadku bezpośredniego dostępu, a new Box<String>() {};nie w przypadku dostępu pośredniego, void foo(T) {...new Box<T>() {};...}ponieważ kompilator nie zachowuje informacji o typie dla deklaracja metody załączającej.
Yann-Gaël Guéhéneuc

Naprawiłem uszkodzony link do mojego artykułu. Powoli odsuwam swoje życie i odzyskuję dane. :-)
Richard Gomes

33

Jeśli masz pole typu ogólnego, jego parametry typu są kompilowane do klasy.

Jeśli masz metodę, która pobiera lub zwraca typ ogólny, parametry tego typu są kompilowane do klasy.

Ta informacja jest tym, czego używa kompilator, aby powiedzieć, że nie można przekazać metody Box<String>do empty(Box<T extends Number>)metody.

API jest skomplikowane, ale można sprawdzić tę informację poprzez API typ refleksji z metod, takich jak getGenericParameterTypes, getGenericReturnTypei na polach getGenericType.

Jeśli masz kod, który używa typu ogólnego, kompilator wstawia rzutowania w razie potrzeby (w programie wywołującym) w celu sprawdzenia typów. Same obiekty ogólne są tylko typem surowym; sparametryzowany typ jest „kasowany”. Tak więc podczas tworzenia new Box<Integer>()nie ma informacji o Integerklasie w Boxobiekcie.

Często zadawane pytania Angeliki Langer to najlepsze referencje, jakie widziałem dla Java Generics.


2
W rzeczywistości jest to formalny rodzajowy rodzaj pól i metod, które są wkompilowane w klasę, tj. Typowe „T”. Aby uzyskać prawdziwy typ typu ogólnego, musisz użyć „sztuczki klasy anonimowej” .
Yann-Gaël Guéhéneuc

13

Ogólne w języku Java jest naprawdę dobrym przewodnikiem na ten temat.

Generyczne są implementowane przez kompilator Java jako konwersja frontonu zwana kasowaniem. Możesz (prawie) myśleć o tym jak o tłumaczeniu między źródłami, przy czym ogólna wersja loophole()jest konwertowana na wersję inną niż ogólna.

Tak więc jest w czasie kompilacji. JVM nigdy nie będzie wiedział, którego ArrayListużyłeś.

Poleciłbym również odpowiedź pana Skeeta na temat: Jaka jest koncepcja wymazywania w generics w Javie?


6

Usuwanie typu następuje w czasie kompilacji. Skasowanie typu oznacza, że ​​zapomni on o typie ogólnym, a nie o każdym typie. Poza tym nadal będą metadane dotyczące typów ogólnych. Na przykład

Box<String> b = new Box<String>();
String x = b.getDefault();

jest konwertowany na

Box b = new Box();
String x = (String) b.getDefault();

w czasie kompilacji. Możesz otrzymać ostrzeżenia nie dlatego, że kompilator wie o typie generycznym, ale wręcz przeciwnie, ponieważ nie wie wystarczająco, więc nie może zagwarantować bezpieczeństwa typu.

Ponadto kompilator zachowuje informacje o typie parametrów w wywołaniu metody, które można pobrać przez odbicie.

Ten przewodnik jest najlepszym, jaki znalazłem na ten temat.


6

Termin „usuwanie typu” nie jest tak naprawdę poprawnym opisem problemu Javy z ogólnymi. Usuwanie typu nie jest samo w sobie złe, w rzeczywistości jest bardzo niezbędne do wydajności i często jest używane w kilku językach, takich jak C ++, Haskell, D.

Przed wstrętem przypomnij sobie poprawną definicję usuwania typu z Wiki

Co to jest kasowanie typu?

usuwanie typu odnosi się do procesu ładowania, w którym jawne adnotacje typu są usuwane z programu, zanim zostanie on wykonany w czasie wykonywania

Usuwanie typu oznacza wyrzucanie znaczników typu utworzonych w czasie projektowania lub wywnioskowanych znaczników typu w czasie kompilacji, tak aby skompilowany program w kodzie binarnym nie zawierał żadnych znaczników typu. Dotyczy to każdego języka programowania kompilującego się do kodu binarnego, z wyjątkiem niektórych przypadków, w których potrzebne są tagi środowiska wykonawczego. Te wyjątki obejmują na przykład wszystkie typy egzystencjalne (typy referencyjne Java, które są podtypów, dowolny typ w wielu językach, typy Unii). Przyczyną kasowania typów jest to, że programy przekształcają się w język, który jest w pewnym sensie jednoznaczny (język binarny dopuszcza tylko bity), ponieważ typy są jedynie abstrakcjami i zapewniają strukturę dla swoich wartości oraz odpowiednią semantykę do ich obsługi.

To jest w zamian normalna naturalna rzecz.

Problem Javy jest inny i wynika z tego, jak się rozwiąże.

Często składane oświadczenia o Javie, które nie mają potwierdzonych nazw ogólnych, są również błędne.

Java naprawia, ale w niewłaściwy sposób ze względu na kompatybilność wsteczną.

Co to jest Reifikacja?

Z naszej Wiki

Reification to proces, w którym abstrakcyjne wyobrażenie o programie komputerowym zamienia się w jawny model danych lub inny obiekt utworzony w języku programowania.

Reification oznacza przekształcenie czegoś abstrakcyjnego (typ parametryczny) w coś konkretnego (typ betonowy) przez specjalizację.

Ilustrujemy to prostym przykładem:

ArrayList z definicją:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

jest abstrakcją, w szczególności konstruktorem typów, który zostaje „zweryfikowany” po specjalizacji z konkretnym typem, powiedzmy Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

gdzie ArrayList<Integer>naprawdę jest typ.

Ale właśnie tego nie robi Java !!! zamiast tego reifikują stale swoje abstrakcyjne typy swoimi granicami, tj. wytwarzając ten sam konkretny typ niezależnie od parametrów przekazywanych do specjalizacji:

ArrayList
{
    Object[] elems;
}

który jest tutaj weryfikowany za pomocą ukrytego obiektu Object ( ArrayList<T extends Object>== ArrayList<T>).

Mimo to sprawia, że ​​tablice ogólne są bezużyteczne i powodują dziwne błędy dla typów surowych:

List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception

powoduje wiele niejasności jak

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

odnoszą się do tej samej funkcji:

void function(ArrayList list)

i dlatego w Javie nie można używać przeciążenia metod ogólnych.


2

Spotkałem się z usuwaniem tekstu w Androidzie. W produkcji stosujemy gradle z opcją minify. Po minifikacji mam fatalny wyjątek. Zrobiłem prostą funkcję, aby wyświetlić łańcuch dziedziczenia mojego obiektu:

public static void printSuperclasses(Class clazz) {
    Type superClass = clazz.getGenericSuperclass();

    Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
    Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));

    while (superClass != null && clazz != null) {
        clazz = clazz.getSuperclass();
        superClass = clazz.getGenericSuperclass();

        Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
        Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
    }
}

I są dwa wyniki tej funkcji:

Kod nieobrobiony:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Kod minimalny:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Tak więc w kodzie zminimalizowanym rzeczywiste sparametryzowane klasy są zastępowane typami klas surowych bez żadnych informacji o typie. Jako rozwiązanie dla mojego projektu usunąłem wszystkie wywołania refleksji i odpowiedziałem im jawnymi typami par przekazanymi w argumentach funkcji.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.