Odpowiedzi:
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) jest List<String>
na przykład a ). W Javie jest to po prostu niewykonalne - możesz dowiedzieć się, że jest to ArrayList
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 (as Class<T>
).
Class<T>
parametr do konstruktora (lub metody ogólnej) po prostu dlatego, że Java nie zachowuje tych informacji. Spójrz na EnumSet.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:
javac -XD-printflat -d output_dir SomeFile.java
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 tylko javac
, 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łych for
pętli itp. Miło jest zobaczyć małe rzeczy, które dzieją się automagicznie.
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();
}
}
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:
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 checkCollection
metodzie 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:
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) ”
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.Iterator
klasy 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();
}
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.
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.
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);
}
}
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>
i List<List<String>>
jest List
.List<Integer>[]
jest List[]
.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 definicji asList
jest Object
, ponieważ T
nie ma granic.T
w definicji max
jest Comparable
, ponieważ T
zostało ograniczone Comparable<? super T>
.T
w ostatecznej definicji max
jest Object
, ponieważ
T
ma 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 :)