Löschen von Java-Generika: Wann und was passiert?

237

Ich habe auf der Oracle-Website über Javas Typlöschung gelesen .

Wann erfolgt die Typlöschung? Zur Kompilierungszeit oder zur Laufzeit? Wann wird die Klasse geladen? Wann wird die Klasse instanziiert?

Viele Websites (einschließlich des oben erwähnten offiziellen Tutorials) geben an, dass die Typlöschung zur Kompilierungszeit erfolgt. Wenn die Typinformationen beim Kompilieren vollständig entfernt werden, wie überprüft das JDK die Typkompatibilität, wenn eine Methode mit Generika ohne Typinformationen oder falsche Typinformationen aufgerufen wird?

Betrachten Sie das folgende Beispiel: Angenommen, die Klasse Ahat eine Methode empty(Box<? extends Number> b). Wir kompilieren A.javaund erhalten die Klassendatei A.class.

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

Jetzt erstellen wir eine weitere Klasse, Bdie die Methode emptymit einem nicht parametrisierten Argument (Rohtyp) aufruft : empty(new Box()). Wenn wir im Klassenpfad B.javamit kompilieren A.class, ist javac klug genug, um eine Warnung auszulösen. So A.class hat irgendeine Art Informationen darin gespeichert.

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());
    }
}

Meine Vermutung wäre, dass das Löschen des Typs auftritt, wenn die Klasse geladen wird, aber es ist nur eine Vermutung. Wann passiert es?

Radiodef
quelle
2
Eine "allgemeinere" Version dieser Frage: stackoverflow.com/questions/313584/…
Ciro Santilli 法轮功 冠状 病 六四 事件 法轮功
@afryingpan: Der in meiner Antwort erwähnte Artikel erklärt ausführlich, wie und wann das Löschen von Typen erfolgt. Außerdem wird erläutert, wann Typinformationen gespeichert werden. Mit anderen Worten: Reified Generics sind in Java verfügbar, entgegen der weit verbreiteten Meinung. Siehe: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Antworten:

240

Die Typlöschung gilt für die Verwendung von Generika. Es gibt definitiv Metadaten in der Klassendatei, die angeben, ob eine Methode / ein Typ generisch ist oder nicht , und welche Einschränkungen es gibt usw. Wenn Generika verwendet werden , werden sie jedoch in Überprüfungen zur Kompilierungszeit und in Ausführungszeiten umgewandelt. Also dieser Code:

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

wird kompiliert in

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

Zur Ausführungszeit gibt es keine Möglichkeit herauszufinden, dass T=Stringfür das Listenobjekt diese Informationen weg sind.

... aber die List<T>Schnittstelle selbst wirbt immer noch als generisch.

BEARBEITEN: Zur Verdeutlichung behält der Compiler die Informationen über die Variable a bei List<String>- aber das können Sie T=Stringfür das Listenobjekt selbst immer noch nicht herausfinden .

Jon Skeet
quelle
6
Nein, selbst bei Verwendung eines generischen Typs sind möglicherweise zur Laufzeit Metadaten verfügbar. Auf eine lokale Variable kann nicht über Reflection zugegriffen werden. Für einen als "List <String> l" deklarierten Methodenparameter stehen jedoch zur Laufzeit Typmetadaten zur Verfügung, die über die Reflection-API verfügbar sind. Ja, "Typ Löschung" ist nicht so einfach, wie viele Leute denken ...
Rogério
4
@ Rogerio: Als ich auf Ihren Kommentar antwortete, glaube ich, dass Sie verwirrt sind zwischen der Möglichkeit, den Typ einer Variablen abzurufen , und der Möglichkeit, den Typ eines Objekts abzurufen . Das Objekt selbst kennt sein Typargument nicht, obwohl das Feld dies tut.
Jon Skeet
Wenn Sie nur das Objekt selbst betrachten, können Sie natürlich nicht wissen, dass es sich um eine Liste <String> handelt. Objekte erscheinen aber nicht nur aus dem Nichts. Sie werden lokal erstellt, als Methodenaufrufargument übergeben, als Rückgabewert von einem Methodenaufruf zurückgegeben oder aus einem Feld eines Objekts gelesen ... In all diesen Fällen können Sie zur Laufzeit auch wissen, um welchen generischen Typ es sich handelt implizit oder mithilfe der Java Reflection-API.
Rogério
13
@ Rogerio: Woher weißt du, woher das Objekt kommt? Wenn Sie einen Parameter vom Typ haben, List<? extends InputStream>wie können Sie dann wissen, welcher Typ es war, als er erstellt wurde? Auch wenn Sie herausfinden können , in welchem ​​Feld die Referenz gespeichert wurde, warum sollten Sie dies tun müssen? Warum sollten Sie in der Lage sein, alle weiteren Informationen zum Objekt zur Ausführungszeit abzurufen, nicht jedoch die generischen Typargumente? Sie scheinen zu versuchen, das Löschen von Schriftarten zu einer winzigen Sache zu machen, die Entwickler nicht wirklich betrifft - obwohl ich es in einigen Fällen als sehr bedeutendes Problem empfinde.
Jon Skeet
Aber das Löschen von Schriftarten ist eine winzige Sache, die Entwickler nicht wirklich betrifft! Natürlich kann ich nicht für andere sprechen, aber nach MEINER Erfahrung war es nie eine große Sache. Ich nutze die Informationen zum Laufzeit-Typ beim Entwurf meiner Java-Mocking-API (JMockit). Ironischerweise scheinen .NET-Spott-APIs das in C # verfügbare generische Typsystem weniger zu nutzen.
Rogério
99

Der Compiler ist dafür verantwortlich, Generics zur Kompilierungszeit zu verstehen. Der Compiler ist auch dafür verantwortlich, dieses "Verständnis" generischer Klassen in einem Prozess, den wir als Typlöschung bezeichnen, wegzuwerfen . Alles geschieht zur Kompilierungszeit.

Hinweis: Entgegen der Meinung der meisten Java-Entwickler ist es möglich, Informationen vom Typ zur Kompilierungszeit beizubehalten und diese Informationen zur Laufzeit abzurufen, obwohl dies sehr eingeschränkt ist. Mit anderen Worten: Java bietet reified Generics auf sehr eingeschränkte Weise .

In Bezug auf die Art der Löschung

Beachten Sie, dass dem Compiler zur Kompilierungszeit vollständige Typinformationen zur Verfügung stehen. Diese Informationen werden jedoch im Allgemeinen absichtlich gelöscht, wenn der Bytecode generiert wird. Dies wird als Typlöschung bezeichnet . Dies geschieht aufgrund von Kompatibilitätsproblemen auf diese Weise: Die Absicht der Sprachentwickler bestand darin, vollständige Quellcodekompatibilität und vollständige Bytecodekompatibilität zwischen Versionen der Plattform bereitzustellen. Wenn es anders implementiert wäre, müssten Sie Ihre Legacy-Anwendungen neu kompilieren, wenn Sie auf neuere Versionen der Plattform migrieren. So wie es gemacht wurde, bleiben alle Methodensignaturen erhalten (Quellcode-Kompatibilität) und Sie müssen nichts neu kompilieren (Binärkompatibilität).

In Bezug auf reified Generics in Java

Wenn Sie Informationen zum Typ der Kompilierungszeit beibehalten müssen, müssen Sie anonyme Klassen verwenden. Der Punkt ist: Im ganz besonderen Fall anonymer Klassen ist es möglich, zur Laufzeit vollständige Informationen zum Typ der Kompilierungszeit abzurufen, was mit anderen Worten bedeutet: reifizierte Generika. Dies bedeutet, dass der Compiler keine Typinformationen wegwirft, wenn anonyme Klassen beteiligt sind. Diese Informationen werden im generierten Binärcode gespeichert, und das Laufzeitsystem ermöglicht es Ihnen, diese Informationen abzurufen.

Ich habe einen Artikel zu diesem Thema geschrieben:

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

Ein Hinweis zu der im obigen Artikel beschriebenen Technik ist, dass die Technik für die Mehrheit der Entwickler unklar ist. Obwohl es funktioniert und gut funktioniert, fühlen sich die meisten Entwickler mit der Technik verwirrt oder unwohl. Wenn Sie eine gemeinsame Codebasis haben oder planen, Ihren Code für die Öffentlichkeit freizugeben, empfehle ich die oben beschriebene Technik nicht. Wenn Sie jedoch der einzige Benutzer Ihres Codes sind, können Sie die Leistung dieser Technik nutzen.

Beispielcode

Der obige Artikel enthält Links zum Beispielcode.

Richard Gomes
quelle
1
@ will824: Ich habe die Antwort massiv verbessert und einen Link zu einigen Testfällen hinzugefügt. Prost :)
Richard Gomes
1
Tatsächlich haben sie nicht sowohl die Binär- als auch die Quellkompatibilität beibehalten: oracle.com/technetwork/java/javase/compatibility-137462.html Wo kann ich mehr über ihre Absicht lesen? Ärzte sagen, dass es Typlöschung verwendet, sagt aber nicht warum.
Dzmitry Lazerka
@ Richard In der Tat, ausgezeichneter Artikel! Sie können hinzufügen, dass auch lokale Klassen funktionieren und dass in beiden Fällen (anonyme und lokale Klassen) Informationen über das gewünschte Typargument nur bei direktem Zugriff gespeichert werdennew Box<String>() {}; nicht bei indirektem Zugriff gespeichert werden, void foo(T) {...new Box<T>() {};...}da der Compiler keine Typinformationen für speichert die beiliegende Methodendeklaration.
Yann-Gaël Guéhéneuc
Ich habe einen defekten Link zu meinem Artikel behoben. Ich google langsam mein Leben und fordere meine Daten zurück. :-)
Richard Gomes
33

Wenn Sie ein Feld haben, das ein generischer Typ ist, werden seine Typparameter in die Klasse kompiliert.

Wenn Sie eine Methode haben, die einen generischen Typ annimmt oder zurückgibt, werden diese Typparameter in die Klasse kompiliert.

Diese Informationen werden vom Compiler verwendet, um Ihnen mitzuteilen, dass Sie a nicht Box<String>an die empty(Box<T extends Number>)Methode übergeben können.

Die API ist kompliziert, aber man kann diese Art Informationen über das Reflection - API mit Methoden wie inspiziert getGenericParameterTypes, getGenericReturnTypeund für Felder getGenericType.

Wenn Sie Code haben, der einen generischen Typ verwendet, fügt der Compiler nach Bedarf (im Aufrufer) Casts ein, um die Typen zu überprüfen. Die generischen Objekte selbst sind nur der Rohtyp; Der parametrisierte Typ wird "gelöscht". Wenn Sie also eine erstellen new Box<Integer>(), gibt es keine Informationen über die IntegerKlasse im BoxObjekt.

Angelika Langers FAQ ist die beste Referenz, die ich für Java Generics gesehen habe.

erickson
quelle
2
Tatsächlich ist es der formale generische Typ der Felder und Methoden, die in die Klasse kompiliert werden, dh typisch "T". Um den realen Typ des generischen Typs zu erhalten, müssen Sie den "Trick der anonymen Klasse" verwenden .
Yann-Gaël Guéhéneuc
13

Generics in Java Language ist eine wirklich gute Anleitung zu diesem Thema.

Generika werden vom Java-Compiler als Front-End-Konvertierung namens Erasure implementiert. Sie können es sich (fast) als eine Übersetzung von Quelle zu Quelle vorstellen, bei der die generische Version von loophole()in die nicht generische Version konvertiert wird.

Es ist also zur Kompilierungszeit. Die JVM wird nie wissen, welcheArrayList Sie verwendet haben.

Ich würde auch die Antwort von Herrn Skeet zu Was ist das Konzept der Löschung in Generika in Java?

Eugene Yokota
quelle
6

Das Löschen des Typs erfolgt zur Kompilierungszeit. Das Löschen von Typen bedeutet, dass der generische Typ und nicht jeder Typ vergessen wird. Außerdem wird es weiterhin Metadaten zu den generischen Typen geben. Beispielsweise

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

wird konvertiert zu

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

zur Kompilierungszeit. Möglicherweise erhalten Sie Warnungen nicht, weil der Compiler weiß, um welchen Typ es sich handelt, sondern im Gegenteil, weil er nicht genug weiß und daher keine Sicherheit für den Typ gewährleisten kann.

Darüber hinaus behält der Compiler die Typinformationen zu den Parametern eines Methodenaufrufs bei, die Sie über Reflection abrufen können.

Dieser Leitfaden ist der beste, den ich zu diesem Thema gefunden habe.

Vinko Vrsalovic
quelle
6

Der Begriff "Typlöschung" ist nicht wirklich die richtige Beschreibung des Problems von Java mit Generika. Das Löschen von Typen ist per se keine schlechte Sache, in der Tat ist es für die Leistung sehr notwendig und wird häufig in mehreren Sprachen wie C ++, Haskell, D verwendet.

Bevor Sie sich abschrecken, erinnern Sie sich bitte an die korrekte Definition der Typlöschung aus dem Wiki

Was ist Typlöschung?

Typlöschung bezieht sich auf den Ladezeitprozess, durch den explizite Typanmerkungen aus einem Programm entfernt werden, bevor es zur Laufzeit ausgeführt wird

Typlöschung bedeutet, dass zur Entwurfszeit erstellte Typ-Tags oder zur Kompilierungszeit abgeleitete Typ-Tags weggeworfen werden, sodass das kompilierte Programm im Binärcode keine Typ-Tags enthält. Dies ist für jede Programmiersprache der Fall, die zu Binärcode kompiliert wird, außer in einigen Fällen, in denen Sie Laufzeit-Tags benötigen. Diese Ausnahmen umfassen beispielsweise alle existenziellen Typen (subtypierbare Java-Referenztypen, Beliebiger Typ in vielen Sprachen, Union-Typen). Der Grund für das Löschen von Typen besteht darin, dass Programme in eine Sprache umgewandelt werden, die in einer Art uni-typisiert ist (Binärsprache erlaubt nur Bits), da Typen nur Abstraktionen sind und eine Struktur für ihre Werte und die entsprechende Semantik für ihre Verarbeitung festlegen.

Das ist also eine normale natürliche Sache.

Das Problem von Java ist anders und hängt davon ab, wie es sich ändert.

Die oft gemachten Aussagen über Java haben keine reifizierten Generika, sind auch falsch.

Java reifiziert zwar, aber aufgrund der Abwärtskompatibilität falsch.

Was ist Verdinglichung?

Aus unserem Wiki

Reifizierung ist der Prozess, durch den eine abstrakte Idee eines Computerprogramms in ein explizites Datenmodell oder ein anderes Objekt umgewandelt wird, das in einer Programmiersprache erstellt wurde.

Reifizierung bedeutet, durch Spezialisierung etwas Abstraktes (Parametrischer Typ) in etwas Konkretes (Konkreter Typ) umzuwandeln.

Wir veranschaulichen dies anhand eines einfachen Beispiels:

Eine ArrayList mit Definition:

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

ist eine Abstraktion, im Detail ein Typkonstruktor, der "spezialisiert" wird, wenn er auf einen konkreten Typ spezialisiert ist, sagen wir Integer:

ArrayList<Integer>
{
    Integer[] elems;
}

Wo ArrayList<Integer>ist wirklich ein Typ.

Aber genau das macht Java nicht !!! Stattdessen reifizieren sie ständig abstrakte Typen mit ihren Grenzen, dh sie produzieren denselben konkreten Typ unabhängig von den zur Spezialisierung übergebenen Parametern:

ArrayList
{
    Object[] elems;
}

Dies wird hier mit dem implizit gebundenen Objekt ( ArrayList<T extends Object>== ArrayList<T>) bestätigt.

Trotzdem macht es generische Arrays unbrauchbar und verursacht einige seltsame Fehler für Rohtypen:

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

es verursacht viele Unklarheiten als

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

beziehen sich auf die gleiche Funktion:

void function(ArrayList list)

Daher kann das generische Überladen von Methoden in Java nicht verwendet werden.

iconfly
quelle
2

Ich habe mit Typ Löschung in Android angetroffen. In der Produktion verwenden wir Gradle mit Minify-Option. Nach der Minimierung habe ich eine fatale Ausnahme. Ich habe eine einfache Funktion zum Anzeigen der Vererbungskette meines Objekts erstellt:

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()));
    }
}

Und es gibt zwei Ergebnisse dieser Funktion:

Nicht minimierter Code:

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

Minimierter Code:

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

In minimiertem Code werden also tatsächlich parametrisierte Klassen durch Rohklassentypen ohne Typinformationen ersetzt. Als Lösung für mein Projekt habe ich alle Reflection-Aufrufe entfernt und durch explizite Parametertypen ersetzt, die in Funktionsargumenten übergeben wurden.

Porfirion
quelle