Type Erasure
Generics existieren eigentlich nur auf Quellcode-Ebene. Nach der Typ-Prüfung etc.
entfernt der Compiler alle generischen Typ-Parameter und alle <...>
(=>
"Type-Erasure"), d.h. im Byte-Code stehen nur noch Raw-Typen bzw. die oberen
Typ-Schranken der Typ-Parameter, in der Regel Object
. Zusätzlich baut der Compiler
die nötigen Casts ein. Als Anwender merkt man davon nichts, muss das "Type-Erasure"
wegen der Auswirkungen aber auf dem Radar haben!
- (K2) Typ-Löschung und Auswirkungen
Typ-Löschung (Type-Erasure)
Der Compiler ersetzt nach Prüfung der Typen und ihrer Verwendung alle Typ-Parameter durch
- deren obere (Typ-)Schranke und
- passende explizite Cast-Operationen (im Byte-Code).
Die obere Typ-Schranke ist in der Regel der Typ der ersten Bounds-Klausel
oder Object
, wenn keine Einschränkungen formuliert sind.
Bei parametrisierten Typen wie List<T>
wird der Typ-Parameter entfernt,
es entsteht ein sogenannter Raw-Typ (List
, quasi implizit mit Object
parametrisiert).
=> Ergebnis: Nur eine (untypisierte) Klasse! Zur Laufzeit gibt es keine Generics mehr!
Hinweis: In C++ ist man den anderen möglichen Weg gegangen und erzeugt für jede Instantiierung die passende Klasse. Siehe Modul "Systemprogrammierung" :)
Beispiel: Aus dem folgenden harmlosen Code-Fragment:
class Studi<T> {
T myst(T m, T n) { return n; }
public static void main(String[] args) {
Studi<Integer> a = new Studi<>();
int i = a.myst(1, 3);
}
}
wird nach der Typ-Löschung durch Compiler (das steht dann quasi im Byte-Code):
class Studi {
Object myst(Object m, Object n) { return n; }
public static void main(String[] args) {
Studi a = new Studi();
int i = (Integer) a.myst(1, 3);
}
}
Die obere Schranke meist Object
=> new T()
verboten/sinnfrei (s.u.)!
Type-Erasure bei Nutzung von Bounds
vor der Typ-Löschung durch Compiler:
class Cps<T extends Number> {
T myst(T m, T n) {
return n;
}
public static void main(String[] args) {
Cps<Integer> a = new Cps<>();
int i = a.myst(1, 3);
}
}
nach der Typ-Löschung durch Compiler:
class Cps {
Number myst(Number m, Number n) {
return n;
}
public static void main(String[] args) {
Cps a = new Cps();
int i = (Integer) a.myst(1, 3);
}
}
Raw-Types: Ich mag meine Generics "well done" :-)
Raw-Types: Instanziierung ohne Typ-Parameter => Object
Stack s = new Stack(); // Stack von Object-Objekten
- Wegen Abwärtskompatibilität zu früheren Java-Versionen noch erlaubt.
- Nutzung wird nicht empfohlen! (Warum?)
Anmerkung
Raw-Types darf man zwar selbst im Quellcode verwenden (so wie im Beispiel
hier), sollte die Verwendung aber vermeiden wegen der Typ-Unsicherheit:
Der Compiler sieht im Beispiel nur noch einen Stack für Object
, d.h. dort
dürfen Objekte aller Typen abgelegt werden - es kann keine Typprüfung
durch den Compiler stattfinden. Auf einem Stack<String>
kann der Compiler
prüfen, ob dort wirklich nur String
-Objekte abgelegt werden und ggf.
entsprechend Fehler melden.
Etwas anderes ist es, dass der Compiler im Zuge von Type-Erasure selbst Raw-Types in den Byte-Code schreibt. Da hat er vorher bereits die Typsicherheit geprüft und er baut auch die passenden Casts ein.
Das Thema ist eigentlich nur noch aus Kompatibilität zu Java5 oder früher da, weil es dort noch keine Generics gab (wurden erst mit Java6 eingeführt).
Folgen der Typ-Löschung: new
new
mit parametrisierten Klassen ist nicht erlaubt!
class Fach<T> {
public T foo() {
return new T(); // nicht erlaubt!!!
}
}
Grund: Zur Laufzeit keine Klasseninformationen über T
mehr
Im Code steht return (CAST) new Object();
. Das neue Object
kann man anlegen, aber ein Cast nach irgendeinem anderen Typ
ist sinnfrei: Jede Klasse ist ein Untertyp von Object
, aber
eben nicht andersherum. Außerdem fehlt dem Objekt vom Typ
Object
auch sämtliche Information und Verhalten, die der
Cast-Typ eigentlich mitbringt ...
Folgen der Typ-Löschung: static
static
mit generischen Typen ist nicht erlaubt!
class Fach<T> {
static T t; // nicht erlaubt!!!
static Fach<T> c; // nicht erlaubt!!!
static void foo(T t) { ... }; // nicht erlaubt!!!
}
Fach<String> a;
Fach<Integer> b;
Grund: Compiler generiert nur eine Klasse! Beide Objekte würden sich die statischen Attribute teilen (Typ zur Laufzeit unklar!).
Hinweis: Generische (statische) Methoden sind erlaubt.
Folgen der Typ-Löschung: instanceof
instanceof
mit parametrisierten Klassen ist nicht erlaubt!
class Fach<T> {
void printType(Fach<?> p) {
if (p instanceof Fach<Number>)
...
else if (p instanceof Fach<String>)
...
}
}
Grund: Unsinniger Code nach Typ-Löschung:
class Fach {
void printType(Fach p) {
if (p instanceof Fach)
...
else if (p instanceof Fach)
...
}
}
Folgen der Typ-Löschung: .class
.class
mit parametrisierten Klassen ist nicht erlaubt!
boolean x;
List<String> a = new ArrayList<String>();
List<Integer> b = new ArrayList<Integer>();
x = (List<String>.class == List<Integer>.class); // Compiler-Fehler
x = (a.getClass() == b.getClass()); // true
Grund: Es gibt nur List.class
(und kein List<String>.class
bzw. List<Integer>.class
)!
Wrap-Up
- Generics existieren eigentlich nur auf Quellcode-Ebene
- "Type-Erasure":
- Compiler entfernt nach Typ-Prüfungen etc.
generische Typ-Parameter etc. => im Byte-Code nur noch Raw-Typen
bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel
Object
- Compiler baut passende Casts in Byte-Code ein
- Transparent für User; Auswirkungen beachten!
- Compiler entfernt nach Typ-Prüfungen etc.
generische Typ-Parameter etc. => im Byte-Code nur noch Raw-Typen
bzw. die oberen Typ-Schranken der Typ-Parameter, in der Regel
- [Bloch2018] Effective Java
Bloch, J., Addison-Wesley, 2018. ISBN 978-0-13-468599-1. - [Java-SE-Tutorial] The Java Tutorials
Oracle Corporation, 2022.
Specialized Trails: Generics - [LernJava] Learn Java
Oracle Corporation, 2022.
Kapitel Generics - [Ullenboom2021] Java ist auch eine Insel
Ullenboom, C., Rheinwerk-Verlag, 2021. ISBN 978-3-8362-8745-6.
Kapitel 11.2 und 11.6