Generics in Java 1.5

 


... [ Seminar BS, WWW und Java ] ... [ Thema Generics in Java ] ... [ Neuerungen in der Reflection API ] ...

Interne Funktionsweise


Raw Types und Konsequenzen

Zur Laufzeit kennt Java keine der im Programmcode vorkommenden Ausprägungen der generischen Typen mehr. Stattdessen wird jede generische Klasse auf ihren sogeannten Raw Type zurückgeführt, und sämtliche generischen Objektreferenzen in normale Referenzen umgewandelt. Der Java Bytecode der resultierenden Klassendateien sieht also prinzipiell genau so wie in den vorherigen Java-Versionen aus (zumindest sind keine Veränderungen durch die Generics hinzugekommen).
Alle Vorkommen von Typparametern innerhalb einer Klasse werden durch den speziellsten (nicht den allgemeinsten; dies wird gleich erläutert) möglichen Typen ersetzt.
Hier noch einmal das Beispiel einer generischen verketteten Liste:
1: public class LinkedList<E> {
2:
3: private E value;
4: private LinkedList<E> next;
5:
6: public LinkedList() {
7: }
8:
9: public boolean isEmpty() {
10: return value == null;
11: }
12:
13: public void add(E value) {
14: // letztes Element?
15: if(isEmpty()) {
16: this.value = value;
17: // neues, leeres Element anhängen
18: next = new LinkedList<E>();
19: }
20: else
21: next.add(value);
22: }
23:
24: public E get(int index) {
25: if(index < 0 || isEmpty())
26: throw new IndexOutOfBoundsException();
27:
28: if(index == 0)
29: return value;
30:
31: return next.get(index - 1);
32: }
33:
34: public int size() {
35: if(isEmpty())
36: return 0;
37:
38: return 1 + next.size();
39: }
40: }
Compiliert man diese Klasse und decompiliert anschließend die entstandene .class-Datei mit einem Java Decompiler, sieht der übriggebliebene Code wie folgt aus:
1: public class LinkedList
2: {
3:
4: private Object value;
5: private LinkedList next;
6:
7: public LinkedList()
8: {
9: }
10:
11: public boolean isEmpty()
12: {
13: return value == null;
14: }
15:
16: public void add(Object obj)
17: {
18: if(isEmpty())
19: {
20: value = obj;
21: next = new LinkedList();
22: } else
23: {
24: next.add(obj);
25: }
26: }
27:
28: public Object get(int i)
29: {
30: if(i < 0 || isEmpty())
31: {
32: throw new IndexOutOfBoundsException();
33: }
34: if(i == 0)
35: {
36: return value;
37: } else
38: {
39: return next.get(i - 1);
40: }
41: }
42:
43: public int size()
44: {
45: if(isEmpty())
46: {
47: return 0;
48: } else
49: {
50: return 1 + next.size();
51: }
52: }
53: }
Wurde der Typparameter bereits eingeschränkt, nutzt der Compiler dies, um statt Object einen spezielleren Typ zu nehmen (daher der speziellste mögliche):
1: class X<E extends Number> {
2: private E value;
3:
4: public E get() {
5: return value;
6: }
7: }
Daraus wird nach Compilation und Decompilation:
1: class X {
2: private Number value;
3:
4: public Number get() {
5: return value;
6: }
7: }
Der Raw Type ist auch derjenige, der verwendet wird, wenn im Programmcode auf die generische Parametrisierung verzichtet wird. Der Vorteil ist ersichtlich: Älterer Java-Code ist komplett kompatibel zu den Generics; er ist nur natürlich weiterhin nicht typsicher (bzw. die Typsicherheit wird nicht garantiert).
Jedoch tun sich hierdurch auch eine ganze Reihe von Problemen auf. Einmal ist dies der Grund, warum Typparameter nicht im statischen Kontext einer Klasse benutzt werden können (außer bei unabhängigen generischen Methoden mit eigenem Typparameter). Zusätzlich resultieren aber auch eine Vielzahl zunächst verwirrender Einschränkungen aus diesem Verhalten. Die Beschränkungen bei der Verwendung von generischen Arrays, die am Ende des letzten Kapitels erwähnt wurden, sind ein Beispiel hierfür. Außerdem ist es z. B. nicht möglich, Methoden anhand von generischen Parametern zu unterscheiden, die nur im Typparameter differieren:
1: import java.util.Vector;
2:
3: class X {
4: public void makeIt(Vector<String> s) { }
5:
6: public void makeIt(Vector<Integer> i) { }
7: }
8:
9: // Error: makeIt(...) and makeIt(...) have the same erasure
Zumindest hierfür kann man sich mit einem kleinen "Workaround" helfen, der jedoch auch keine optimale Lösung darstellt. Man kann sich Klassen definieren, die von der betroffenen generischen Klasse erben und bei der Vererbung bereits einen konkreten Typparameter angeben. So sind diese Klassen selbst nicht mehr generisch, und der Compiler kann sie unterscheiden. Sie dienen quasi als "Alias" für die konkreten Ausprägungen der generischen Klasse.
1: class StringVector extends Vector<String> {
2: // ggf. Konstruktoren erben
3: public StringVector(int initialCapacity) {
4: super(initialCapacity);
5: }
6: }
7:
8: class IntVector extends Vector<Integer> { }
9:
10: class X {
11: public void makeIt(StringVector s) { }
12:
13: public void makeIt(IntVector i) { }
14: }
Um bei der Verwendung von generischen Containern die Methoden der Elemente korrekt aufrufen zu können, muss der Compiler wieder Typecasts einbauen. Würde man die oben gezeigte verkettete Liste mit Integer parametrisieren, würden die Elemente, besser gesagt, die Rückgabewerte der Methode get() ja u. a. die Methode intValue() bereitstellen. Da get() im Bytecode jedoch, wie oben ersichtlich, nur noch ein Object liefert, muss der Compiler zusätzlich einen Downcast einbauen:
1: LinkedList<Integer> intList = new LinkedList<Integer>();
2: intList.add(new Integer(42));
3: intList.get(0).intValue();
Resultierender Code nach Compilation:
1: LinkedList linkedlist = new LinkedList();
2: linkedlist.add(new Integer(42));
3: ((Integer)linkedlist.get(0)).intValue();
Da also die Downcasts, die durch die Generics im Programmcode nicht mehr nötig sind, noch immer vorhanden sind, ist hier durch die Einführung der Generizität keinerlei Performancegewinn erzielt worden.
Da jedoch auch keine zusätzlichen Klassen, wie z. B. in C++ bei der Verwendung von Templates, erzeugt werden, ist zumindest auch kein Performanceverlust durch die Verwendung von Generics zu beklagen.

instanceof

Dadurch, dass die generischen Objekte auf den entsprechenden Raw Type zurückgeführt werden (man spricht auch vom erased Type der Ausprägungen), sind natürlich auch keine generischen Abfragen per instanceof möglich:
1: List<Integer> li = new Vector<Integer>();
2:
3: (li instanceof Vector<Integer>) == true; // error: illegal generic type for instanceof
4: (li instanceof Vector) == true; // OK
Dies führt dazu, dass man unter einigen Umständen davon ausgehen muss, dass ein Objekt von einem bestimmten generischen Typ ist, und es explizit darauf casten muss. Damit ist ein Ausgangsproblem wieder vorhanden. In diesem Falle spricht man von unchecked casts.

Unchecked Casts

Angenommen, es werden generische Objekte zur Laufzeit serialisiert (das Wissen um die allgemeine Serialisierung von Objekten wird an dieser Stelle vorausgesetzt). Werden diese Objekte später wieder deserialisiert, ist es logischerweise nicht möglich, festzustellen, ob diese Objekte wirklich vom generischen Typ sind, der benötigt wird.
Die Methode readObject der Klasse ObjectInputStream liefert eine Object-Referenz zurück. Diese kann man natürlich per instanceof beispielsweise daraufhin überprüfen, ob es sich wirklich um eine Referenz auf ein Vector-Objekt handelt. Mit welchem Typparameter dieses Vector-Objekt parametrisiert wurde, muss man jedoch schlicht voraussetzen, was zu einer Warnung des Compilers führt:
1: Vector<String> vs = new Vector<String>();
2: vs.add("Text 1");
3: vs.add("Text zwei");
4:
5: ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.dat"));
6: oos.writeObject(vs);
7: oos.close();
8:
9: ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.dat"));
10: Object o = ois.readObject();
11: if(o instanceof Vector)
12: vs = (Vector<String>)o; // warning: unchecked cast
13: ois.close();

Brückenmethoden

Folgendes Codebeispiel:
1: class X<E> {
2: private Integer test;
3:
4: public Integer get(E param) {
5: return test;
6: }
7:
8: }
9:
10: class Y extends X<String> {
11:
12: public Integer get(String param) {
13: return new Integer(10);
14: }
15: }
In Zeile 4 deklariert die Klasse X eine Methode get(), die als Parameter ein Objekt vom Typen des Typparameters der Klasse erwartet. Klasse Y erbt nun von der Klasse X, setzt dabei aber für den Typparameter String ein. Anschließend scheint die Klasse Y in Zeile 12 die Methode get() zu überschreiben, da die Methode im Rahmen der Vererbung die gleiche Signatur hat wie die get()-Methode der Basisklasse X.
Erinnert man sich nun jedoch daran, wie der Compiler die Klasse X übersetzt, folgt daraus, dass dort nur noch ein Object als Parameter der get()-Methode steht. Die Methode der Klasse Y hat also nach der Compilation eine andere Signatur.
Da der Programmierer hiervon aber prinzipiell nichts weiß, simuliert der Compiler die Überschreibung mit einer sogenannten Brückenmethode. Der compilierte und decompilierte Code sieht wie folgt aus:
1: class X {
2:
3: private Integer test;
4:
5: public X() { }
6:
7: public Integer get(Object obj) {
8: return test;
9: }
10: }
11:
12: class Y extends X {
13:
14: public Y() { }
15:
16: public Integer get(String s) {
17: return new Integer(10);
18: }
19:
20: public Integer get(Object obj) {
21: return get((String)obj);
22: }
23: }
Die Methode in Zeile 20 wurde vom Compiler eingefügt. Sie überschreibt die Methode der Klasse X und ruft lediglich die theoretisch überschreibende Methode der Klasse Y auf. Somit verhält sich die Klasse Y so, als wäre die Methode überschrieben worden.

Die Brückenmethoden werden sogar bei Methoden angewendet, die sich (bei Rückführung auf Raw Types) nur im Rückgabewert unterscheiden:
1: interface Iterator<E> {
2: public boolean hasNext();
3:
4: public E next();
5: }
6:
7: class Alphabet implements Iterator<Character> {
8: char current = 'A';
9:
10: public boolean hasNext() { return current <= 'Z'; }
11:
12: public Character next() {
13: return new Character(current++);
14: }
15: }
Das Interface Iterator definiert eine allgemeine Methode next(), die eine Objektreferenz vom Typ des Typparameters liefert. Der Compiler macht hieraus wieder einen Object-Rückgabewert.
Die Klasse Alphabet implementiert die Schnittstelle, jedoch nur für Zeichen. Die Methode next(), die folgerichtig ein Zeichen liefert, sollte also das next() aus der Schnittstelle Iterator korrekt implementieren. Selbst hier erzeugt der Compiler eine Brückenmethode:
1: interface Iterator {
2: public boolean hasNext();
3:
4: public Object next();
5: }
6:
7: class Alphabet implements Iterator {
8:
9: char current;
10:
11: Alphabet() {
12: current = 'A';
13: }
14:
15: public boolean hasNext() {
16: return current <= 'Z';
17: }
18:
19: public Character next() {
20: return new Character(current++);
21: }
22:
23: public Object next() {
24: return next();
25: }
26: }
Dieser Code wäre im Programmcode nicht erlaubt, da die Bedingung gilt, dass verschiedene Methoden innerhalb einer Klasse sich durch mehr als nur ihren Rückgabewert unterscheiden müssen. In Java Bytecode ist dies jedoch tatsächlich möglich.

... [ Seminar BS, WWW und Java ] ... [ Thema Generics in Java ] ... [ Interne Funktionsweise ] ... [ Neuerungen in der Reflection API ] ...

Valid XHTML 1.0 Strict