it-swarm.com.de

Was ist das Konzept der Löschung in Generika in Java?

Was ist das Konzept der Löschung in Generika in Java?

136
Tushu

Es ist im Grunde die Art und Weise, wie Generika in Java über Compiler-Tricks implementiert werden. Der kompilierte generische Code tatsächlich verwendet nur Java.lang.Object, Wo immer Sie über T (oder einen anderen Typparameter) sprechen - und es gibt einige Metadaten, die dem Compiler mitteilen, dass dies der Fall ist ist wirklich ein generischer Typ.

Wenn Sie Code mit einem generischen Typ oder einer generischen Methode kompilieren, ermittelt der Compiler, was Sie wirklich meinen (dh was das Typargument für T ist), und überprüft dies zum Zeitpunkt compile Sie tun das Richtige, aber der ausgegebene Code spricht wieder nur von Java.lang.Object - der Compiler generiert bei Bedarf zusätzliche Casts. Zur Ausführungszeit sind ein List<String> Und ein List<Date> Genau gleich. Die zusätzlichen Typinformationen wurden gelöscht vom Compiler.

Vergleichen Sie dies beispielsweise mit C #, in dem die Informationen zur Ausführungszeit beibehalten werden und der Code Ausdrücke wie typeof(T) enthalten kann, die T.class Entsprechen - mit der Ausnahme, dass letzteres ungültig ist. (Es gibt wohlgemerkt weitere Unterschiede zwischen .NET-Generika und Java Generika.) Die Typlöschung ist die Quelle für viele der "merkwürdigen" Warn-/Fehlermeldungen beim Umgang mit Java Generika .

Andere Ressourcen:

192
Jon Skeet

Nur als Randnotiz ist es eine interessante Übung, zu sehen, was der Compiler tatsächlich macht, wenn er Löschvorgänge ausführt - macht das gesamte Konzept ein wenig verständlicher. Es gibt ein spezielles Flag, das Sie dem Compiler übergeben können, um Java Dateien auszugeben, bei denen die Generika gelöscht und Casts eingefügt wurden. Ein Beispiel:

javac -XD-printflat -d output_dir SomeFile.Java

Das -printflat ist das Flag, das an den Compiler übergeben wird, der die Dateien generiert. (Das -XD Teil ist das, was javac anweist, es der ausführbaren JAR-Datei zu übergeben, die das Kompilieren ausführt, anstatt nur javac, aber ich schweife ab ...) Das -d output_dir ist erforderlich, da der Compiler einen Platz zum Ablegen der neuen .Java-Dateien benötigt.

Dies ist natürlich mehr als nur eine Löschung. Hier werden alle automatischen Aufgaben des Compilers erledigt. Zum Beispiel werden auch Standardkonstruktoren eingefügt, die neuen foreach-artigen for-Schleifen werden zu regulären for-Schleifen usw. erweitert. Es ist schön zu sehen, welche kleinen Dinge automatisch passieren.

40
jigawot

Löschen bedeutet wörtlich, dass die im Quellcode vorhandenen Typinformationen aus dem kompilierten Bytecode gelöscht werden. Lassen Sie uns dies mit etwas Code verstehen.

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

Wenn Sie diesen Code kompilieren und dann mit einem Java Decompiler dekompilieren, erhalten Sie so etwas. Beachten Sie, dass der dekompilierte Code keine Spur der im Original vorhandenen Typinformationen enthält Quellcode.

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

    }
} 
28
Parag

Um die bereits sehr vollständige Antwort von Jon Skeet zu vervollständigen, müssen Sie das Konzept von type erasure aus einem Bedürfnis von Kompatibilität mit früheren Versionen von Java .

Ursprünglich auf der EclipseCon 2007 vorgestellt (nicht mehr verfügbar), umfasste die Kompatibilität die folgenden Punkte:

  • Quellkompatibilität (Schön zu haben ...)
  • Binäre Kompatibilität (Muss sein!)
  • Migrationskompatibilität
    • Bestehende Programme müssen weiterhin funktionieren
    • Bestehende Bibliotheken müssen generische Typen verwenden können
    • Haben müssen!

Ursprüngliche Antwort:

Daher:

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

Es gibt Vorschläge für eine größere Verdinglichung. Reify als "Betrachten Sie ein abstraktes Konzept als real", wo Sprachkonstrukte Konzepte sein sollten, nicht nur syntaktischer Zucker.

Ich sollte auch die Methode checkCollection von Java 6 erwähnen, die eine dynamisch typsichere Ansicht der angegebenen Sammlung zurückgibt. Jeder Versuch, ein Element des falschen Typs einzufügen, führt zu einem sofortigen ClassCastException.

Der generische Mechanismus in der Sprache bietet eine (statische) Typprüfung zur Kompilierungszeit, aber es ist möglich, diesen Mechanismus mit ungeprüften Casts zu umgehen.

In der Regel ist dies kein Problem, da der Compiler bei allen solchen ungeprüften Vorgängen Warnungen ausgibt.

Es gibt jedoch Zeiten, in denen die statische Typprüfung allein nicht ausreicht, wie zum Beispiel:

  • wenn eine Sammlung an eine Bibliothek eines Drittanbieters übergeben wird und der Bibliothekscode die Sammlung nicht durch Einfügen eines Elements des falschen Typs beschädigen darf.
  • ein Programm schlägt mit einem ClassCastException fehl, was darauf hinweist, dass ein falsch eingegebenes Element in eine parametrisierte Auflistung eingefügt wurde. Leider kann die Ausnahme jederzeit nach dem Einfügen des fehlerhaften Elements auftreten, sodass in der Regel nur wenige oder keine Informationen zur tatsächlichen Ursache des Problems bereitgestellt werden.

Update Juli 2012, fast vier Jahre später:

Es ist jetzt (2012) detailliert in " API-Migrationskompatibilitätsregeln (Signaturtest) ".

Die Programmiersprache Java implementiert Generics mithilfe von Erasure, wodurch sichergestellt wird, dass ältere und generische Versionen mit Ausnahme einiger Zusatzinformationen zu Typen in der Regel identische Klassendateien generieren. Die Binärkompatibilität wird nicht beeinträchtigt, da sie ersetzt werden kann Eine Legacy-Klassendatei mit einer generischen Klassendatei, ohne den Clientcode zu ändern oder neu zu kompilieren.

Um die Schnittstelle mit nicht generischem Legacy-Code zu vereinfachen, ist es auch möglich, das Löschen eines parametrisierten Typs als Typ zu verwenden. Ein solcher Typ wird als Rohtyp ( Java Language Specification 3/4.8 ) bezeichnet. Wenn Sie den RAW-Typ zulassen, wird auch die Abwärtskompatibilität für den Quellcode sichergestellt.

Demnach sind die folgenden Versionen des Java.util.Iterator Klasse sind sowohl Binär- als auch Quellcode abwärtskompatibel:

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

Ergänzt die bereits ergänzte Antwort von Jon Skeet ...

Es wurde erwähnt, dass das Implementieren von Generika durch Löschen zu einigen ärgerlichen Einschränkungen führt (z. B. no new T[42]). Es wurde auch erwähnt, dass der Hauptgrund für diese Vorgehensweise die Abwärtskompatibilität im Bytecode war. Dies gilt auch (meistens). Der erzeugte Bytecode -Ziel 1.5 unterscheidet sich etwas von dem gezuckerten Casting -Ziel 1.4. Technisch gesehen ist es sogar möglich (durch immense Tricks), auf generische Typinstanziierungen zuzugreifen zur Laufzeit, um zu beweisen, dass der Bytecode wirklich etwas enthält.

Der interessantere Punkt (der noch nicht angesprochen wurde) ist, dass die Implementierung von Generika mithilfe von Erasure wesentlich flexibler ist, als das High-Level-Typ-System dies leisten kann. Ein gutes Beispiel hierfür wäre die JVM-Implementierung von Scala im Vergleich zu CLR. In der JVM können höhere Typen direkt implementiert werden, da die JVM selbst keine Einschränkungen für generische Typen festlegt (da diese "Typen" praktisch nicht vorhanden sind). Dies steht im Gegensatz zur CLR, die Laufzeitkenntnisse über Parameterinstanziierungen besitzt. Aus diesem Grund muss die CLR selbst ein Konzept für die Verwendung von Generika haben, das Versuche zunichte macht, das System mit unerwarteten Regeln zu erweitern. Infolgedessen werden die höheren Arten von Scala in der CLR mithilfe einer seltsamen Form der Löschung implementiert, die im Compiler selbst emuliert wird, sodass sie nicht vollständig mit einfachen alten .NET-Generika kompatibel sind.

Das Löschen kann unpraktisch sein, wenn Sie zur Laufzeit ungezogene Aktionen ausführen möchten, bietet den Compiler-Autoren jedoch die größte Flexibilität. Ich schätze, das ist ein Teil dessen, warum es nicht so schnell weggeht.

8
Daniel Spiewak

Wie ich es verstehe (ein . NET Typ zu sein), hat das JVM kein Konzept von Generika, daher ersetzt der Compiler Typparameter durch Objekt und führt das gesamte Casting für Sie aus.

Dies bedeutet, dass Java Generics nichts als Syntaxzucker sind und keine Leistungsverbesserung für Werttypen bieten, die ein Boxing/Unboxing erfordern, wenn sie als Referenz übergeben werden.

5
Andrew Kennan

Es gibt gute Erklärungen. Ich füge nur ein Beispiel hinzu, um zu zeigen, wie die Typlöschung mit einem Dekompiler funktioniert.

Ursprüngliche Klasse,

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

Dekompilierter Code aus seinem Bytecode,

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


   }
}
2
snr