javaseiten.de   |   Version 0.6
 

3.8. Type Erasure

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 javac compiliert werden. Anschließend soll die entstandene Klassendatei mit Hilfe eines geeigneten Programms (Java-Decompiler) wieder decompiliert werden, um das Quellprogramm des erzeugten Bytecodes zu erhalten. Der Quellcode von GenericsExample.class könnte wie folgt aussehen:

// 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).

 

Type Erasure bei Typparamtern

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 extends Number für den Typparameter E verwendet. Anstatt von NumberHolder<E extends Number> wird der Raw Type NumberHolder verwendet und der allgemeine Typparameter E wurde durch Number ersetzt. Wird der Typparameter nicht gebunden (es wird keine Angabe über eine Schranke gemacht), erscheint Object anstelle von E. Type Erasure von ungebundenen Typparametern resultiert also im Typ Object.

 

Brückenmethoden

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 den parametrisierten Typ Holder<String> erweitern (siehe auch Listing 3.2):

/* HolderExtends.java */
public class HolderExtends extends Holder<String> {
  public String str;

  public void set(String s) {
    str = s;
  }  
}

Die Datei HolderExtends.java kann anschließend mit dem Java-Compiler javac übersetzt und danach kann HolderExtends.class wieder decompiliert werden. Im dadurch entstandenen "neuen" Quellcode erscheint eine zusätzliche Methode, eine Brückenmethode:

// 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 HolderExtends definiert die Methode set, deren einziger Parameter vom Typ String ist und erweitert Holder<String>. Es könnte angenommen werden, dass damit die set-Methode von Holder.java ebenfalls nur einen einzigen Paramter besitzt, der auch vom Typ String ist. Die Signatur der in HolderExtends definierten Methode und der von Holder<String> geerbten Methode scheint gleich zu sein und damit wird die letztere überlagert. Wie aber aus dem Abschnitt Type Erasure bei Typpparametern hervorgeht, wird public void set(E v) durch den Compiler durch public void set(Object v) ersetzt. Der Typ des einzigen Parameters der zu vererbenden Methode set ist vom Typ Object und damit würde auch keine Überlagerung dieser Methode stattfinden. Dies ist ein unerwünschter Nebeneffekt des Type Erasure: Die beiden betrachteten Methoden haben vor dem Typ Erasure die gleiche Signatur, besitzen aber nach dem Typ Erasure unterschiedliche Signaturen (und dies ist nicht erwünscht). Um diesen Nebeneffekt auszugleichen wird eine Brückenmethode in die erbende Klasse aufgenommen. Die durch den Compiler generierte Brückenmethode hat die gleiche Signatur der zu vererbenden Methode nach dem Type Erasure. Sie leitet zur Methode set der Klasse HolderExtends weiter. Da eine Brückenmethode eine Art Hilfsmittel ist, kann diese nicht wie eine gewöhnliche Methode aufgerufen werden, obwohl sie eigentlich vorhanden ist.

 

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 linkextern.gif). Die Methodensignatur ist ausschlaggebend, ob eine Methode bei der Vererbung von Klassen überschrieben (überlagert) wird. Eine Methodensignatur wird durch die folgenden Größen fixiert:

  • Namensbezeichnung der Methode

  • Anzahl und Typ der (formalen) Parameter der Methode

  • Anzahl und Schranken (Bounds) der Typparameter der Methode

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. T1 für E und T2 für S). Eine Signatur kann nun derart qualifiziert sein, dass sie eine Subsignatur einer anderen Methodensignatur ist. Eine Subsignatur beschreibt also eine Abhängigkeit zwischen zwei Signaturen unterschiedlicher Methoden. Dabei ist Subsignatur wie folgt definiert: Eine Signatur ist eine Subsignatur einer anderen Signatur, falls eine der beiden Bedingungen zutrifft:

  • die beiden Signaturen sind gleich

  • die eine Signatur ist gleich dem "Erasure der anderen Signatur"

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:

  • s1 ist eine Subsignatur von s2

  • s2 ist eine Subsignatur von s1

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 paraVector und minmax. Zusätzlich erweitert die Klasse GenericMethodOverrider die Klasse GenericMethodExample (siehe Listing 3.12), in der ebenfalls Methoden mit den Namen paraVector und minmax deklariert sind. Im folgenden soll "GMO" eine Abkürzung für GenericMethodOverrider sein und "GME" steht für GenericMethodExample. Die Signatur von GMO:paraVector ist eine Subsignatur der Signatur von GME:paraVector und daher wird diese Methode in der erbenden Klasse überschrieben. Die Signaturen der Methoden minmax in den jeweiligen Klassen sind keine Subsignaturen zur jeweils anderen und es findet deshalb keine Überschreibung stattt (die Klasse GenericMethodOverrider stellt also zwei Methoden mit dem Namen minmax zur Verfügung; eine Methode wird definiert und eine wird geerbt).

Listing 3.15. GenericMethodOverrider.java. Beispiel zu Subsignaturen von Methodensignaturen.

/* 
 * 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:

  Vor dem Type Erasure Nach dem Type Erasure
GME: Methodendeklaration public <E> Vector<E> paraVector() public Vector paraVector()
GMO: Methodendeklaration public Vector paraVector() public Vector paraVector()
GME: Methodensignatur <T1>paraVector() paraVector()
GMO: Methodensignatur paraVector() paraVector()

An dieser Gegenüberstellung wird deutlich, dass folgendes gilt: Die Methodensignaturen von GME:paraVector und GMO:paraVector sind override-equivalent, weil die Singnatur von GMO:paraVector eine Subsignatur der Signatur der Methode GME:paraVector ist.

  Vor dem Type Erasure Nach dem Type Erasure
GME: Methodendeklaration public <E extends Number & Comparable<E>> E minmax(Vector<E> v, boolean mm) public Number minmax(Vector v, boolean mm)
GMO: Methodendeklaration public <E> E minmax(Vector<E> v, boolean mm) public Object minmax(Vector v, boolean mm)
GME: Methodensignatur <T1_extends_Number​_&_Comparable​<T1_extends_Number​_&_Comparable>>​minmax(Vector​<T1_extends_Number​_&_Comparable>,​boolean) minmax(Vector,​boolean)
GMO: Methodensignatur <T1>minmax​(Vector<T1>,​boolean) minmax(Vector,​boolean)

Die Methode GME:minmax hat nicht die gleiche Signatur wie GMO:minmax, denn der Typparameter E der ersten Methode hat die Schranke extends Number & Comparable<E> währenddessen der Typparameter E der zweiten Methode keine Schranke besitzt. Zusätzlich ist keine der Signaturen der beiden betrachteten Methoden gleich dem "Erasure der Signatur" der jeweils anderen. Es folgert sich daraus, dass die beiden Signaturen keine Subsignaturen der jeweils anderen sind. Daraus folgt: Die beiden Methodensignaturen von GMO:minmax und GME:minmax sind nicht override-equivalent und eine Überlagerung findet deshalb nicht statt.

 

 

 

Diese Seite nutzt Google-Dienste - siehe dazu Datenschutz.

Copyright © 2006, 2007 Harald Roeder