Type Erasure ist ein Mechanismus, der bei der Übersetzung von generischem Java-Quellcode durch den Compiler zur Anwendung kommt. Type Erasure bezeichnet dabei eine Zuordnung (Mapping) von Typen zu anderen Typen. Der Quellcode enthält z.B. vor dem Type Erasure Typparamter und parametrisierte Typen. Nach dem Type Erasure sind jedoch keine Typparamter und parametrisierte Typen mehr vorhanden. Zusätzlich fügt der Compiler bei Bedarf in den Quellcode Typüberprüfungen, Typumwandlungen oder sogenannte Brückenmethoden ein. Type Erasure bewirkt sozusagen zunächst eine Übersetzung von generischem Quellcode in einen regulären Quellcode (keine generischen Elemente vorhanden), der anschließend in einen entsprechenden Bytecode compiliert wird.
Type Erasure bei parametrisierten Typen Als nächstes soll erläutert werden, wie sich der Mechanismus Type Erasure
auf Typargumente auswirkt. Dazu soll Listing 3.1 zunächst
mittels // DeCompiled : GenericsExample.class import java.util.*; public class GenericsExample { public static void main(String args[]) { Vector v = new Vector(); v.add(new Integer(3)); Integer i = (Integer)v.get(0); System.out.println(i.toString()); } } Dabei wurden durch den Compiler die folgenden Änderungen vorgenommen (das aktuelle Typargument wurde entfernt und eine Typumwandlung wurde zusätlich eingefügt): vorher: Vector<Integer> v = new Vector<Integer>(); nachher: Vector v = new Vector(); vorher: Integer i = v.get(0); nachher: Integer i = (Integer)v.get(0); Type Erasure von parametrisierten Typen führt zu einem Typ ohne Typargumente (Raw Type).
Um das Verhalten des Compilers beim Auftreten von Typparamtern im Quellcode zu untersuchen, soll Listing 3.2 zunächst in Bytcode übersetzt werden und anschließend wieder decompiliert werden. Danach lautet der Quellcode: // DeCompiled : Holder.class public class Holder { private Object value; public void set(Object v) { value = v; } public Object get() { return value; } } Ein Vergleich zwischen dem Quellcode vorher und dem Quellcode nachher zeigt, dass durch den Java-Übersetzer folgendes modifiziert wurde (1.Saplte): Holder.java NumberHolder.java vorher: public class Holder<E> public class NumberHolder<E extends Number> nachher: public class Holder public class NumberHolder voher: private E value; private E value; nachher: private Object value; private Number value; voher: public void set(E v) public void set(E v) nachher: public void set(Object v) public void set(Number v) vorher: public E get() public E get() nachher: public Object get() public Number get() In der 2. Spalte wurde Listing 3.9 auch zunächst
compiliert und danach wieder decompiliert. Im Listing wurde die Schranke
Brückenmethoden sind zusätzlich durch den Compiler erzeugte Methoden, die
durch den Vorgang des Type Erasure benötigt werden, wenn z.B. eine Klasse eine
parametrisierte Klasse erweitert. Zunächst soll die Klasse /* HolderExtends.java */ public class HolderExtends extends Holder<String> { public String str; public void set(String s) { str = s; } } Die Datei // DeCompiled : HolderExtends.class public class HolderExtends extends Holder { public String str; public void set(String s) { str = s; } public volatile void set(Object obj) { // Brueckenmethode! set((String)obj); } } Dabei stellt sich die Frage: Warum wurde eine zusätzliche Methode (Brückenmethode) durch den Compiler generiert? Eine Antwort auf diese Frage liefert das Konzept des Type Erasure. Annahme: Holder<String> --> public void set(String v) { ... } Real: decompiled Holder.class --> public void set(Object v) { ... } HolderExtends.java --> public void set(String s) { ... } Der Compiler fügt gegebenenfalls Brückenmethoden in Klassen ein, die von
parametrisierten Typen abgeleitet wurden. Dadurch sollen die Regeln für die
Vererbung von Methoden nicht verletzt werden. Die Klasse
Subsignatur einer Methodensignatur Durch Type Erasure wird eine Erweiterung des Begriffes "Signatur einer Methode" nötig. Es wurde daher die Bezeichnung der Subsignatur einer Signatur eingeführt (siehe Java Language Specification ). Die Methodensignatur ist ausschlaggebend, ob eine Methode bei der Vererbung von Klassen überschrieben (überlagert) wird. Eine Methodensignatur wird durch die folgenden Größen fixiert:
Die folgenden zwei Beispiele zeigen Methodendeklarationen und die dazugehörigen Signaturen: Methode: public String hansImGlueck(int anzahl, boolean b) { ... } Signatur: hansImGlueck(int,boolean) Methode: public <E extends Number> E berechnung(Vector<E> v, S type, boolean mm) { ... } Signatur: <T1_extends_Number>berechnung(Vector<T1_extends_Number>,T2,boolean) Die konkreten Namensbezeichnungen von Typparametern werden zur Vereinfachung
durch den Compiler einheitlich durchnummeriert (z.B.
Durch Type Erasure innerhalb von Methoden wird auch die Signatur dieser Methode verändert. In diesem Zusammenhang wird vom "Erasure einer Signatur" gesprochen. Der Begriff Subsignatur kann nun verwendet werden, um die Äquivalenz bezüglich Überschreibung (Override-Equivalence) zweier Methodensignaturen festzulegen: Zwei Methodensignaturen s1 und s2 sind override-equivalent, falls eine der beiden Bedingungen gilt:
Sind also zwei Methoden in unterschiedlichen Klassen und die eine Klasse
erweitert die andere, kann die eine Methode die andere überschreiben auch wenn die
beiden Methodensignaturen nicht identisch sind. Für eine Überlagerung reicht
aus, dass die eine Methodensignatur eine Subsignatur der Signatur der anderen
Methode ist. Dazu soll ein Beispiel betrachtet werden. Das anschließende Listing
enthält die beiden Methoden Listing 3.15. /* * GenericMethodOverrider.java * JDK 5 * */ import java.util.*; public class GenericMethodOverrider extends GenericMethodExample { public Vector paraVector() { Vector v = new Vector(); System.out.println("GenericMethodOverrider, paraVector"); return v; } public <E> E minmax(Vector<E> v, boolean mm) { Iterator<E> iter = v.iterator(); E a = iter.next(); System.out.println("GenericMethodOverrider, minmax"); return a; } public static void main(String[] args) { GenericMethodOverrider gmo = new GenericMethodOverrider(); Vector v = gmo.paraVector(); Vector<Integer> vi = new Vector<Integer>(); vi.add(new Integer("12")); vi.add(new Integer("4")); vi.add(new Integer("11")); Integer imin = gmo.minmax(vi, false); System.out.println(imin.toString()); Vector<String> vs = new Vector<String>(); vs.add("Hans im Glueck"); String str = gmo.minmax(vs, true); System.out.println(str); } } Die Ausgabe des Beispiels lautet wie folgt: GenericMethodOverrider, paraVector GenericMethodExample, minmax 4 GenericMethodOverrider, minmax Hans im Glueck Konkret an diesem Beispiel soll der Vorgang des Type Erasure und des Signature Erasure nachvollzogen werden:
An dieser Gegenüberstellung wird deutlich, dass folgendes gilt:
Die Methodensignaturen von GME:
Die Methode GME: |
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||