Jaka jest koncepcja wymazywania w produktach generycznych w Javie?

141

Jaka jest koncepcja wymazywania w produktach generycznych w Javie?

Tushu
źródło

Odpowiedzi:

200

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.Objectwszę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:

Jon Skeet
źródło
6
@Rogerio: Nie, obiekty nie będą miały różnych typów ogólnych. Te pola znać typy, ale obiekty nie.
Jon Skeet
8
@Rogerio: Absolutnie - niezwykle łatwo jest dowiedzieć się w czasie wykonywania, czy coś, co jest dostarczane tylko jako Object(w scenariuszu słabo wpisanym) jest List<String>na przykład a ). W Javie jest to po prostu niewykonalne - możesz dowiedzieć się, że jest to ArrayListtyp, 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 (as Class<T>).
Jon Skeet
6
Nigdy nie twierdziłem, że to zawsze lub prawie zawsze problem - ale przynajmniej dość często jest to problem z mojego doświadczenia. Są różne miejsca, w których jestem zmuszony dodać Class<T>parametr do konstruktora (lub metody ogólnej) po prostu dlatego, że Java nie zachowuje tych informacji. Spójrz na EnumSet.allOfprzykł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)
Jon Skeet
5
Zanim użyłem typów generycznych .NET, stwierdziłem, że generyczne programy Java są pod różnymi względami niezręczne (a symbole wieloznaczne nadal są bólem głowy, chociaż forma wariancji „określana przez dzwoniącego” ma zdecydowanie zalety) - ale dopiero po tym, jak użyłem typów ogólnych .NET przez chwilę widziałem, jak wiele wzorców stało się niewygodnych lub niemożliwych w przypadku generycznych elementów języka Java. To znowu paradoks Blub. Nie mówię, że typy generyczne .NET również nie mają wad, przy okazji - istnieją różne relacje typów, których niestety nie można wyrazić - ale zdecydowanie wolę je od generycznych Java.
Jon Skeet
5
@Rogerio: Wiele można zrobić z refleksją - ale nie wydaje mi się, żebym chciał robić te rzeczy prawie tak często, jak rzeczy, których nie mogę zrobić z generycznymi językami Java. Nie chcę znaleźć argumentu typu dla pola prawie tak często, jak chcę znaleźć argument typu rzeczywistego obiektu.
Jon Skeet
41

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:

javac -XD-printflat -d output_dir SomeFile.java

Jest -printflatto flaga przekazywana kompilatorowi, który generuje pliki. ( -XDCzęść jest tym, co mówi, javacaby przekazać go do pliku wykonywalnego jar, który faktycznie kompiluje, a nie tylko javac, ale dygresuję ...) Jest -d output_dirto 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 forpętle w stylu foreach są rozszerzane do zwykłych forpętli itp. Miło jest zobaczyć małe rzeczy, które dzieją się automagicznie.

jigawot
źródło
29

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.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

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.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 
Parag
źródło
Próbowałem użyć dekompilatora java, aby zobaczyć kod po usunięciu typu z pliku .class, ale plik .class nadal zawiera informacje o typie. Próbowałem jigawotpowiedzieć, to działa.
szczery
25

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:

  • Zgodność źródeł (miło mieć ...)
  • Zgodność binarna (musi mieć!)
  • Zgodność migracji
    • Istniejące programy muszą nadal działać
    • Istniejące biblioteki muszą mieć możliwość korzystania z typów ogólnych
    • Muszę mieć!

Oryginalna odpowiedź:

W związku z tym:

new ArrayList<String>() => new ArrayList()

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 checkCollectionmetodzie Java 6, która zwraca dynamicznie bezpieczny widok określonej kolekcji. Każda próba wstawienia elementu niewłaściwego typu spowoduje natychmiastowy ClassCastException.

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:

  • gdy kolekcja jest przekazywana do biblioteki innej firmy i konieczne jest, aby kod biblioteki nie uszkodził kolekcji przez wstawienie elementu niewłaściwego typu.
  • program kończy się niepowodzeniem i ClassCastExceptionwskazuje, ż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)

Język programowania Java implementuje typy generyczne za pomocą wymazywania, co zapewnia, że ​​starsze i ogólne wersje zazwyczaj generują identyczne pliki klas, z wyjątkiem niektórych pomocniczych informacji o typach. Kompatybilność binarna nie jest zepsuta, ponieważ istnieje możliwość zastąpienia starszego pliku klas zwykłym plikiem klas bez zmiany lub ponownej kompilacji kodu klienta.

Aby ułatwić współpracę ze starszym kodem innym niż ogólny, można również użyć wymazywania sparametryzowanego typu jako typu. Taki typ nazywa się typem surowym ( specyfikacja języka Java 3 / 4.8 ). Zezwolenie na typ surowy zapewnia również zgodność z poprzednimi wersjami kodu źródłowego.

Zgodnie z tym następujące wersje java.util.Iteratorklasy są wstecznie kompatybilne zarówno z kodem binarnym, jak i kodem źródłowym:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
VonC
źródło
2
Zauważ, że kompatybilność wsteczną można było osiągnąć bez usuwania typów, ale nie bez uczenia się przez programistów Java nowego zestawu kolekcji. To jest dokładnie droga, którą przeszedł .NET. Innymi słowy, to ta trzecia kula jest najważniejsza. (Ciąg dalszy.)
Jon Skeet,
15
Osobiście uważam, że był to błąd krótkowzroczności - dał krótkoterminową przewagę i długoterminową wadę.
Jon Skeet
8

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.

Daniel Śpiewak
źródło
6
Niedogodność nie występuje wtedy, gdy chcesz robić „niegrzeczne” rzeczy w czasie wykonywania. To wtedy, gdy chcesz robić całkiem rozsądne rzeczy w czasie wykonywania. W rzeczywistości wymazywanie typu pozwala na wykonywanie znacznie bardziej niegrzecznych rzeczy - takich jak rzutowanie List <String> na List, a następnie na List <Date> z tylko ostrzeżeniami.
Jon Skeet
5

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.

Andrew Kennan
źródło
3
Generics Java i tak nie mogą reprezentować typów wartości - nie ma czegoś takiego jak List <int>. Jednak w Javie nie ma żadnego przekazywania przez odniesienie - jest to ściśle przekazywane przez wartość (gdzie ta wartość może być odniesieniem).
Jon Skeet,
2

Istnieją dobre wyjaśnienia. Dodam tylko przykład, aby pokazać, jak działa wymazywanie typu z dekompilatorem.

Oryginalna klasa,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

Zdekompilowany kod z jego kodu bajtowego,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}
snr
źródło
2

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:

  • Silniejsze sprawdzanie typów w czasie kompilacji.
  • Eliminacja odlewów.
  • Umożliwienie programistom implementacji ogólnych algorytmów.

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:

  • Zastąp wszystkie parametry typu w typach ogólnych ich granicami lub Object, jeśli parametry typu są nieograniczone. Dlatego utworzony kod bajtowy zawiera tylko zwykłe klasy, interfejsy i metody.
  • W razie potrzeby wstawić odlewy, aby zachować bezpieczeństwo typu.
  • Generuj metody pomostowe, aby zachować polimorfizm w rozszerzonych typach ogólnych.

[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:

  • Wymazywanie List<Integer>, List<String>i List<List<String>>jest List.
  • Usunięcie List<Integer>[]jest List[].
  • Usunięcie Listjest samo w sobie, podobnie dla każdego typu surowego.
  • Usunięcie int jest samo w sobie, podobnie dla każdego typu pierwotnego.
  • Usunięcie Integerjest samo w sobie, podobnie dla każdego typu bez parametrów typu.
  • Skasowanie Tw definicji asListjest Object, ponieważ T nie ma granic.
  • Usunięcie Tw definicji maxjest Comparable, ponieważ T zostało ograniczone Comparable<? super T>.
  • Usunięcie Tw ostatecznej definicji maxjest Object, ponieważ Tma związane Object&, 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.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

Zamierzamy ten kod działać w następujący sposób:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Jednak w tym przypadku wymazywanie podpisów obu metod jest identyczne:

boolean allZero(List)

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 :)

Atif
źródło