Java 5.0
Andreas Eberhart, Stefan Kraus, Ulrich
Walther, Vasu Chandrasekhara, Horst
Hellbrück
Konzepte, Grundlagen und Erweiterungen in 5.0
ISBN 3-446-22946-9
Leseprobe
Weitere Informationen oder Bestellungen unter
http://www.hanser.de/3-446-22946-9 sowie im Buchhandel
Kapitel 5
Spracherweiterungen der Java
Version 5.0
5.1 Generische Datentypen
Die Einführung generischer Datentypen, oft auch Generics, Generizität, parametrisierte oder generische Datentypen genannt, ist wahrscheinlich eine der am längsten erwarteten Spracherweiterungen von Java. Am bekanntesten sind generische
Datentypen sicher aus der Welt von C++, wo die so genannten Templates (zu
deutsch etwa Schablonen) die Nutzung generischer Datentypen ermöglichen und
schon vom ersten Tag an in der Sprachspezifikation von C++ enthalten waren.
Hierbei muss erwähnt werden, dass die Realisierung von Templates in C++ jedoch völlig unterschiedlich im Vergleich zur Realisierung generischer Typen in
Java ist. Hierzu im nächsten Abschnitt mehr.
Sehr früh nach Veröffentlichung des JDK 1.0 im Jahre 1996 wurden jedoch alternative Spracherweiterungen wie etwa Pizza schon im gleichen Jahr 1996 bekannt,
die unter anderem bereits parametrisierte Typen unterstützten und in regulären
Java-Bytecode umsetzten. Aus Pizza ging schließlich auch die Weiterentwicklung
GJ (Generic Java) hervor, die letztlich die Grundlage für die Generics im neuen
Java in der Version 5.0 darstellt.
Zunächst einmal soll die Frage beantwortet werden, warum man denn generische
Datentypen überhaupt benutzen soll. Da man die ersten acht Jahre in Java auch
ohne auskam, ist dies sicherlich eine berechtigte Frage.
Durch die Vererbungshierarchie sind in Java alle Klassen Spezialisierungen der
Klasse Object, daher kann man einen Platzhalter für einen beliebigen (nicht primitiven) Datentyp stets als Typ Object deklarieren. Da es viele Algorithmen gibt,
die man unabhängig vom Datentyp spezifizieren kann, wie etwa das Suchen in
oder Sortieren von Listen, ist es sinnvoll, diese Algorithmen etwa auf Arrays oder
260
5 Spracherweiterungen der Java Version 5.0
Listen vom Typ Object operieren zu lassen. Denn dann kann man diesen Algorithmus mit beliebigen Eingabetypen füttern und muss ihn nicht für jeden Typ
nochmals neu aufschreiben. Diese Eigenschaft wird auch Polymorphie oder Polymorphismus (griechisch Vielgestaltigkeit) genannt. Die dazugehörigen Klassen
heißen dann polymorphe Klassen, die zugehörigen Methoden polymorphe Methoden.
Leider birgt die Realisierung der Polymorphie mittels Datentypen, hier mit dem
Typ Object (oder allgemein eines passenden Supertyps), als Platzhalter eine
gewisse Gefahr: beim Sortieralgorithmus etwa erwartet man ein Array, welches
mit Objekten genau eines Typs gefüllt wurde. Was passiert jedoch, wenn sowohl
String- als auch Integer-Objekte in dem Array übergeben werden? Zum Zeitpunkt der Übersetzung des Programmes passiert gar nichts, da der Compiler ein
Array vom Objects erwartet und dies auch so übergeben wird. Zur Laufzeit jedoch wird nun ein Fehler auftreten, sobald der Vergleich zwischen Integer und
String stattfinden soll – dies ist sehr unangenehm, heißt es doch, dass man
möglichst alle Pfade eines Programmes austesten muss, um Laufzeitfehler dieser Art zu entdecken. Und gerade dieses Testen aller Laufpfade ist schlichtweg
unmöglich in komplexeren (realistischen) Applikationen.
Hier versprechen nun generische Datentypen Abhilfe, indem sie diese Prüfungen
schon zur Übersetzungszeit erledigen können. Hierfür wird nun ein Platzhalterdatentyp als so genannter Typparameter verwendet, mit dem der Algorithmus
aufgeschrieben wird. Unser Sortierbeispiel etwa bekommt nicht mehr Object[]
(ein Array von Objects), sondern T[] (ein Array von Objekten des Platzhaltertypes T) übergeben. T steht nun für einen generischen Typ, der erst bei Nutzung der
polymorphen Klasse oder Methode als Parameter angegeben wird. Das Array von
T kann jedoch nur Objekte des Types T 1 beinhalten, und damit stellt der Übersetzer sicher, dass das oben beschriebene Laufzeitproblem nicht mehr auftreten kann
und Fehler schon zur Übersetzungszeit entdeckt werden.
Gerade das Java Collections Framework stellt eine vielzahl polymorpher Methoden und Klassen zur Verfügung und ist dadurch für die in diesem Kapitel beschriebenen Erweiterungen prädestiniert (siehe Kapitel 4.7 für eine Einführung in
das Collections Framework und Kapitel 6.1 für die Erweiterungen, die sich durch
die generischen Datentypen ergeben).
5.1.1 Parametrisierte oder generische Datentypen
In C++ wurde die Implementierung von Templates im Übersetzer – stark vereinfacht ausgedrückt – als Textersetzer realisiert. Dies bedeutet, dass für jede Nutzung einer generischen Klasse vom Typ T für jeden benutzten Typ einfach T durch
den benutzten Typ ersetzt, und eine neue Klasse in die Objektdatei hinzugefügt
wird. Wenn nun also für 20 verschiedene Datentypen die generische Klasse benutzt wird, heißt dies auch, dass in C++ zwanzigmal übersetzter Maschinencode
1 Oder
auch Unterklasen des Typs T
5.1 Generische Datentypen
261
für diese Klasse vorhanden ist und damit die Größe der ausführbaren Applikation
aufbläht (man spricht auch von Code Bloat). Dies wollte man bei Java vermeiden
und ging einen anderen Weg.
Die Grundidee für parametrisierte oder generische Typen besteht darin, eine Klasse schreiben zu können, die als Parameter eine andere oder mehrere Klassen (Datentypen) hat. Wie eingangs erwähnt, nennt man diese dann polymorphe Klasse.
Diese Typvariablen stehen nun für verschiedene Typen, die als Platzhalter für verschiedene Typen benutzt werden können. Polymorphie konnte man mit den bisherigen Sprachmitteln von Java in der Version 1.4 auch schon realisieren, jedoch
mit einigen kleinen Nachteilen. Definieren wir etwa einen Stack (deutsch auch
Kellerspeicher) für beliebige Datentypen in traditionellem Java:
3
public class Stack
{
private class StackStorage {
Object object;
StackStorage prev;
}
StackStorage top;
8
public Stack() { top = new StackStorage(); }
public void push( Object obj ) {
StackStorage store = new StackStorage();
store.prev = top;
store.object = obj;
}
public Object pop() {
Object ret = top.object;
top = top.prev;
return ret;
}
13
18
}
Der Stack wird hier über eine verkettete Liste des internen Datentyps (der inneren
Klasse) StackStorage realisiert, deren Feld prev jeweils auf das vorherige Element
weist und deren Feld object den Speicher für das tatsächliche Element darstellt.
Im diesem definierten Stack können wir nun Objekte jeden beliebigen Typs speichern, doch ist das insofern unschön, weil man dadurch jegliche Typinformation und somit auch Typsicherheit zur Übersetzungszeit verliert. Das liegt daran,
dass man zur Speicherung von beliebigen Typen die Superklasse Object verwendet hat. Denn die Method pop liefert uns als Ergebnis den Typ Object zurück,
wodurch wir selbst implizit wissen müssen, was wir vorher hineingesteckt haben. Zu allem Überfluss müssen wir den Ergebnistyp auch noch auf den richtigen
Typ casten. Betrachten wir folgendes Beispiel, in dem wir einen Stack für Strings
nutzen wollen:
262
4
9
5 Spracherweiterungen der Java Version 5.0
class OldStackUser
{
public static void main(String[] args) {
Stack aStack = new Stack();
aStack.push( "hallo" );
aStack.push( "welt" );
String s = (String) aStack.pop();
Integer i = (Integer) aStack.pop();
}
}
Die letzte Zeile der Methode useTheStack liefert uns einen Laufzeitfehler, weil das
zurückgegebene Element eben ein String ist und daher nicht in den Typ Integer
gewandelt werden kann. Wir wissen das, der Übersetzer kann es jedoch nicht
wissen.
examples>java OldStackUser
Exception in thread "main" java.lang.ClassCastException:
java.lang.String
at OldStackUser.main(OldStackUser.java:8)
Mit Hilfe der generischen Typen kann man nun polymorphe Klassen definieren, die ebenso allgemein wie das obige traditionelle Beispiel eingesetzt werden
können, jedoch die statische Typsicherheit schon zur Übersetzungszeit garantieren. Hierzu fügt man nun zur Klassendefinition eine Typvariable zur Parametrisierung der Klasse Stack ein, die wir für unser Beispiel etwa einfach T nennen
wollen:
1
6
public class Stack<T> {
private class StackStorage {
T object;
StackStorage prev;
}
StackStorage top;
public Stack() { top = new StackStorage(); }
public void push( T obj ) {
StackStorage store = new StackStorage();
store.prev = top;
store.object = obj;
}
public T pop() {
T ret = top.object;
top = top.prev;
return ret;
}
11
16
}
5.1 Generische Datentypen
263
Wie man sieht, werden die Typparameter in spitzen Klammern angegeben. Den
Typparameter kann man nun innerhalb der Klassendefinition als Platzhalter eines
Datentyps verwenden 2 . In unserem Fall sieht das so aus, dass überall dort, wo
vorher der Supertyp Object verwendet wurde, nun T eingesetzt wird.
Die Übersetzung des alten Stack-Benutzers OldStackUser funktioniert nach wie
vor – hier hilft uns die Generizität noch nicht weiter. Denn auch der Benutzer der
Klasse muss die neuen generischen Typen nutzen. Insofern sehen wir hier: Man
kann Klassen mit den neuen Erweiterungen entwickeln und auch bestehenden
Code mit generischen Typen verfeinern, ohne existierende Benutzer der Klassen
neu übersetzen oder ändern zu müssen. Dies ist ein großer Vorteil des benutzten
Modells zur Realisierung von Generizität in Java.
Wie sieht nun ein Nutzer der generischen Eigenschaften von Stack aus? Hier unser
altes Beispiel mit Nutzung der Erweiterungen:
5
class NewStackUser {
public static void main(String[] args) {
Stack aStack<String> = new Stack<String>();
aStack.push( "hallo" );
aStack.push( "welt" );
String s = aStack.pop();
// Fehler: Integer i = (Integer) aStack.pop();
}
}
Hier sagen wir also explizit, dass der Stack nur Objekte des Typs (oder Untertyps)
String speichern darf. Der Übersetzer hat nun jederzeit die volle Typinformation
zur Verfügung, was darin resultiert, dass keine Elemente auf den Stack gepusht
werden dürfen, die keine Strings sind, und dass keinerlei Typwandlung bei Aufruf der Methode pop mehr notwendig ist. Da der Übersetzer den Rückgabetyp
kennt, kann er auch den Type-Cast selbst einsetzen. Die ursprüngliche fehlerhafte
Zeile liefert nun ebenfalls wie erwünscht einen Fehler zur Übersetzungszeit und
nicht erst zur Laufzeit des Programmes:
examples>javac -source 5.0 Stack.java
NewStackUser.java:12: incompatible types
found
: java.lang.String
required: java.lang.Integer
Integer i = (Integer) aStack.pop();
Zusammenfassung der Syntax für die Angabe von Typparametern
Die folgenden Arten von parametrisierten Typen sind in Java möglich:
<T>
Der Typ T hat keine Schranke (unbound).
2 Jedoch
nicht in statischen Initialisierern, dort ist der Typparameter unbekannt.
264
5 Spracherweiterungen der Java Version 5.0
<T,U>
Die beiden Typen T und U haben keine Schranke (sind beide unbound).
<T extends JButton>
Der Typ T ist ein JButton oder eine Unterklasse von JButton.
<T extends Action>
Der Typ T implementiert die Schnittstelle Action.
<T extends InputStream & ObjectInput>
T ist eine Unterklasse von InputStream und implementiert die Schnittstelle
ObjectInput.
<T extends Comparable<T>>
T implementiert die generische Schnittstelle Comparable bezüglich T.
<T, S super T>
Zwei Typen werden definiert, wobei S in Abhängigkeit von T definiert wird
und vom Typ T oder einer Unterklasse von T sein muss.
Zusätzlich zu den hier gezeigten Möglichkeiten gibt es noch die so genannten
Wildcards, die in Abschnitt 5.1.3 eingeführt werden.
5.1.2 Typwandlung zwischen generischen Klassen
Nehmen wir mal an, wir haben folgende beiden Stacks definiert:
1
Stack<String> stringStack;
Stack<Object> objectStack;
Intuitiverweise würde man erwarten, da Object ein Supertyp von String ist, dass
nun auch der Stack von Objects ein Supertyp des Stacks von Strings ist. Dies ist
aber absichtlich nicht der Fall und die Zuweisung liefert eine Fehlermeldung vom
Übersetzer. Warum ist das so? Schauen wir uns mal folgendes Programmfragment
an:
stringStack.push( "allerlei" );
3
objectStack = stringStack;
objectStack.push( 5 );
String doesntWork = stringStack.pop();
Würde der Übersetzer den Supertyp anerkennen und die Zuweisung akzeptieren, so würde die statische Typsicherheit hierdurch wiederum unterwandert werden. Aus diesem Grund wurde in der Spezifikation explizit diese Beziehung zwischen parametrisierten Datentypen eben nicht als Supertyp definiert. Diese strikte Einhaltung des Typsystems schränkt aber an anderer Stelle wieder ein. Wenn
5.1 Generische Datentypen
265
man nun etwa einen Stack<Number> definiert, erwartet man auch, dass ein
Stack<Integer> ebenfalls ein solcher ist. Will man eine Methode schreiben, die
nun die beiden obersten Zahlen vom Stack nimmt und deren Summe berechnet,
so geht dies mit der bisherigen Definition nicht. Hier muss nun also eine flexiblere
Definition der Typparameter her.
5.1.3 Wildcards für parametrisierte Typen
Der erste Ansatz sähe eigentlich wie folgt aus. Man schreibt eine Methode, die als Parameter einen Stack<Number> bekommt, und hofft so, auch
Stack<Integer> übergeben zu dürfen:
4
static double sum( Stack<Number> stack ) {
Number op1 = stack.pop();
Number op2 = stack.pop();
return op1.doubleValue() + op2.doubleValue();
}
Leider liefert das aus dem im vorigen Abschnitt erläuterten Grund einen Fehler des Übersetzers. Jegliche Angabe eines Supertypes von einem Typparameter
wird aus diesem Grund ebenfalls nicht funktionieren. Daher wurde das Fragezeichen als Wildcard für eine Klasse unbekannten Typs eingeführt. Die Superklasse
aller Stacks lautet mit dieser Syntax also Stack<?>. Wenn nun noch die Unterklasseneigenschaft des Parametertyps, oder etwa die Implementierung einer bestimmten Schnittstelle, ausgedrückt werden soll, so kann dies durch Angabe des
Schlüsselwortes extends getan werden. In unserem Fall lautet die Lösung zur
Angabe eines generischen Stacks, der Objekte vom Typ Number speichert, also
Stack<? extends Number>. Das Beispiel sieht also nun so aus:
5
static double sum( Stack<? extends Number> stack ) {
Number op1 = stack.pop();
Number op2 = stack.pop();
return op1.doubleValue() + op2.doubleValue();
}
Diese Methode kann nun tatsächlich alle möglichen Stacks, die Objekte des Typs
Number speichern, füttern; sie ist trotzdem absolut typsicher in der Überprüfung
des Übersetzers:
Stack<Integer> intStack = new Stack<Integer>();
Stack<Float> floatStack = new Stack<Float>();
Stack<BigDecimal> bigStack = new Stack<BigDecimal>();
5
intStack.push(3); intStack.push(4);
System.out.println( sum(intStack) );
floatStack.push(1.5f); floatStack.push(2.5f);
System.out.println( sum(floatStack) );
266
5 Spracherweiterungen der Java Version 5.0
10
bigStack.push( new BigDecimal("4567") );
bigStack.push( new BigDecimal("891011") );
System.out.println( sum(bigStack) );
Selbstverständlich kann eine parametrisierte Klasse auch mehrere Typparameter
haben, und sogar rekursive Typdeklarationen sind erlaubt:
2
class Triple<S,T,U> {
protected S first;
protected T second;
protected U third;
S getFirst() { return first; }
T getSecond() { return second; }
U getThird() { return third; }
7
}
12
class SpecialListTriple
<List<S extends Serializable>,
List<T extends Comparable>,
List<U extends S>> {
...
}
5.1.4 Parametrisierte Schnittstellen
In der gleichen Art und Weise, wie man Klassen parametrieren kann, geschieht
dies auch mit Schnittstellendefinitionen (Interfaces). Wir können etwa eine generische Schnittstelle für unseren Stack wie folgt definieren:
4
interface IStack<T> {
public void push( T object );
public T pop();
}
Nun können wir unsere Implementierung des generischen Stacks dieses Interface
ebenfalls implementieren lassen:
1
public class Stack<T> implements IStack<T>
{
...
}
Es sind sonst keinerlei Änderungen in der Implementierung der Klasse notwendig, denn die Signaturen unserer Stack-Implementierung stimmen ja mit denen
der Schnittstelle überein. Natürlich kann man auch spezielle Implementierungen
der Schnittstelle deklarieren, wie etwa in folgendem Beispiel:
5.1 Generische Datentypen
1
6
267
public class IntegerStack implements IStack<Integer> {
private class StackStorage
{
Integer object;
StackStorage prev;
}
StackStorage top;
public IntegerStack() { top = new StackStorage(); }
11
public void push( Integer obj ) {
StackStorage store = new StackStorage();
store.prev = top;
top = store;
store.object = obj;
}
public Integer pop() {
Integer ret = top.object;
top = top.prev;
return ret;
}
16
21
}
Hier wurde also die spezielle Schnittstelle IStack<Integer> implementiert
und somit in der Implementierung vollständig auf generische Datentypen verzichtet. Gleichzeitiges Implementieren mehrerer Schnittstellen des gleichen Basistyps ist allerdings verboten; das Folgende geht also nicht:
2
class WhatAMistake implements IStack<Integer>, IStack<String> {
...
}
5.1.5 Parametrisierte Methoden
Wie schon eingangs erwähnt, handelt es sich bei Java, technisch betrachtet, bei der
Einführung von Generics um eine Erweiterung der Sprache, damit die Prüfung
der Typsicherheit bereits zur Übersetzungszeit stattfinden kann.
Lassen Sie uns hier als Beispiel zunächst einmal eine Standardaufgabe lösen: die
Bestimmung der Position eines Objektes in einem gegebenen Array (sprich: die
Suche des Objektes im gegebenen Array). Dies geschieht üblicherweise in einer
Methode, die sowohl das zu durchsuchende Array als auch das Objekt, nach welchem gesucht werden soll, übergeben bekommt:
2
/*
* Sucht das Objekt im gegebenen Array.
* @return -1 falls nicht gefunden, Index im Array sonst.
268
7
5 Spracherweiterungen der Java Version 5.0
*/
static int search( Object[] array, Object object )
{
for (int i=0; i<array.length; i++)
if ( object.equals(array[i]) )
return i;
return -1;
}
Da wir den allen Objekten zugrunde liegenden Supertyp Object als Parameter
gewählt haben, können wir nun die Suche für beliebige Arrays und Typen aufrufen:
Integer meinIntArray[];
Long meinLongArray[];
4
int posInt = search( meinIntArray, new Integer(4) );
int posLong = search( meinLongArray, new Long(-1000L) );
Leider liefert uns der traditionelle Java-Übersetzer keinen Fehler, wenn wir Unsinn treiben:
Integer meinIntArray[];
int posInt = search( meinIntArray, "meinSuchText" );
Genau diesen Fall wollen wir nun durch die Nutzung der Erweiterungen abdecken. Hierbei nutzen wir nun die Syntax-Erweiterung von Methodensignaturen: eine Methode, die einen generischen Typ benutzt, muss diesen in der Signatur, ähnlich wie bei parametrisierten Klassen, in spitzen Klammern angeben. Sonst
sieht die Methode beinahe identisch wie die oben angegebene aus, statt Object
wird lediglich jeweils der generische Typ verwendet:
3
8
/*
* Sucht das Objekt im gegebenen Array.
* @return -1 falls nicht gefunden, Index im Array sonst.
*/
static <T> int search( T[] array, T object )
{
for (int i=0; i<array.length; i++)
if ( object.equals(array[i]) )
return i;
return -1;
}
Die ersten Aufrufbeispiele funktionieren nun immer noch wie erwünscht, aber
das zweite Beispiel liefert nun während der Übersetzung einen Fehler, weil die
Typen der beiden Parameter nicht übereinstimmen.
5.1 Generische Datentypen
269
5.1.6 Sonderfälle
Schreiben wir doch unser Beispiel von der Sortierung eines Arrays einfach einmal
auf konventionelle Art und Weise auf:
4
9
14
/*
* Sortiert das übergebene Array und liefert
* als Ergebnis ein sortiertes Array zurück.
* @return Sortiertes Array
*/
static Comparable[] sortArray( Comparable[] input )
{
Comparable[] result = new Comparable[ input.length ];
for (int i=0; i<input.length; i++)
{
// hier wird sortiert
}
return result;
}
Da zur Sortierung der Vergleich zwischen den zu sortierenden Elementen erforderlich ist, wird hier der Supertyp Comparable benutzt, der diese Eigenschaft fordert.
Hat man nun ein Array von Integer und ein anderes Array von Long-Objekten,
so kann man dieselbe Methode zum Sortieren verwenden:
Integer meinIntArray[];
Long meinLongArray[];
5
Integer[] iResult = (Integer[]) sortArray( meinIntArray );
Long[] lResult = (Long[]) sortArray( meinLongArray );
Es fällt unschön auf, dass wegen der Deklaration des Ergebnisses als
Comparable[] jeweils der Ergebnistyp entsprechend dem erwarteten Ergebnis
gecastet werden muss. Auch das birgt wiederum einen Laufzeitfehler, etwa wenn
die sortArray-Methode gar kein Array, wie erwartet, sondern – wegen eines
Fehlers etwa in der Programmlogik – einen anderen Arraytyp als Ergebnis liefert.
Oder wenn die Methode mit einem Array anderen Typs aufgerufen wurde. Auch
hier hilft uns der Einsatz von generischen Datentypen.
Hier nun also die gleiche Methode unter Zuhilfenahme der neuen generischen
Datentypen:
4
/*
* Sortiert das übergebene Array und liefert
* als Ergebnis ein sortiertes Array zurück.
*/
static <T extends Comparable> T[] sortArray( T[] input )
270
5 Spracherweiterungen der Java Version 5.0
{
// Erzeugt FEHLER beim Übersetzen:
T[] result = new T[ input.length ];
for (int i=0; i<input.length; i++)
{
// hier wird sortiert
}
return result;
9
14
}
So würde man die obige Methode intuitiv aufschreiben, jedoch sofort in eine Falle laufen: die Erzeugung des Ergebnisarrays funktioniert nicht wie erwartet, der
Übersetzer verweigert die Annahme des new-Statements mit dem Parametertyp
T. Woran liegt das? Nun, intern wird eine Methode mit generischen Datentypen
einfach in die äquivalente Methode mit Object als Platzhalter übersetzt, also genau
in das, was wir zuvor als Beispiel ohne Generics aufgeschrieben hatten. Das Problem liegt nun darin, dass ein new T[Anzahl] in ein new Object[Anzahl]
umgesetzt würde – aber damit genau ein Array von Objects erzeugen würde, was
aber der Typsicherheit widerspricht. Aus diesem Grund wurden diese Sonderfälle
vom Übersetzer verboten, der Benutzer muss hier selbst ein Array mit passendem Datentyp erzeugen. Dies kann man über die Methode newInstance aus
der Klasse java.lang.reflect.Array wie folgt erledigen:
1
6
// Erzeugt ein Array von Objekten des
// generischen Typs:
T[] result = (T[]) Array.newInstance(
input.getClass().getComponentType(),
input.length );
Dies funktioniert, weil nun explizit der Typ von T mittels getClass geholt wird
und aufbauend auf diesem ein Array erzeugt wird. Unser einfaches Beispiel wird
nun leider etwas unschön, aber anders ist es leider nicht zu erledigen. Um Sie
gleich zu beruhigen: die Erzeugung von neuen Objekten mit generischem Typ
ist die einzige Ausnahme, die beachtet werden muss, und es geschieht immer
nach dem hier gezeigten Schema. Unser komplettes, tatsächlich funktionierendes
Beispiel sieht nun also so aus:
4
9
/*
* Sortiert das übergebene Array und liefert
* als Ergebnis ein sortiertes Array zurück.
*/
static <T extends Comparable> T[] sortArray( T[] input )
{
T[] result = (T[]) Array.newInstance(
input.getClass().getComponentType(),
input.length );
5.1 Generische Datentypen
271
for (int i=0; i<input.length; i++)
{
// hier wird sortiert
}
return result;
14
}
Unerlaubte Konstrukte
Die Tatsache, dass der Übersetzer die generischen Datentypen in Java dadurch
realisiert, dass er den so genannten Erasure-Typ des generischen Datentypes ermittelt (also in den meisten Fällen faktisch die spitzen Klammern weglässt), führt
zu einigen Ausnahmen und nicht erlaubten Fällen, die nicht direkt offensichtlich
sind. Dieser Abschnitt zeigt die häufigsten dieser Fälle auf:
Es sind keine generischen Datentypen im statischen Kontext erlaubt.
Insbesondere folgende Fälle erzeugen einen Fehler bei der Übersetzung:
class GenericStaticTest<T>
{
static T meineVariable; // Fehler
4
static T gibWasZurueck()
{
...
}
// Fehler
9
static void arbeiteWas( List<T> liste ) // Fehler
{
...
}
14
}
Typparameter dürfen nicht überladen werden.
Also darf man Folgendes nicht definieren:
1
// Fehler
class EineKlasse<T> {...}
class EineKlasse<U, V, W> {...}
Ebenso wenig wie die Überladung von Methoden:
1
6
class Paar< E, Z > {
// Fehler
void setze( E erster ) {...}
void setze( Z zweiter ) {...}
}
272
5 Spracherweiterungen der Java Version 5.0
Klasseninformation zur Laufzeit, Realisierung generischer Typen
Die immer gestellte Preisfrage zur Implementierung der generischen Datentypen
in Java ist die: Was druckt das angegebene Codefragment aus?
2
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
Auf den ersten Blick würde man sofort false vermuten, da es sich um unterschiedliche Datentypen handelt. Auf Grund der Implementierung, nämlich das
einfache Löschen der Parameterinfo eines generischen Typs3 und Nutzung dieses Typs, werden allerdings alle generischen Typen List<T> letztendlich auf
den Datentyp List abgebildet. Das heißt auch, dass von einer generischen Klasse tatsächlich nur eine einzige Implementierung vorhanden ist, nämlich die mit
dem Erasure als Grunddatentyp. In unserem Beispielfall wird also tatsächlich nur
Bytecode für die Klasse List erzeugt. Dies ist komplett unterschiedlich zu der
Herangehensweise von C++, wo für jeden benutzten generischen Typ einer Klasse eine eigene Implementierung erzeugt wird (die so genannte Template Instantiation). Aus diesem Grund sind am Ende tatsächlich l1 und l2 beide vom Typ
List, sodass die Klasseninformationen natürlich identisch sind und somit der
obige Ausdruck tatsächlich true liefert.
Der Übersetzer nutzt also bei der Erzeugung des Java Bytecodes prinzipiell immer den Datentyp mit gelöschter Parameterliste, und wo nötig, fügt er noch Typwandlungen in den entsprechenden resultierenden Datentyp ein. Es gibt noch
kleinere Sonderfälle, die jedoch zum Verständnis weniger wichtig sind und daher hier nicht ausgeführt werden.
5.1.7 Wildcards oder parametrisierte Methoden?
Oft gibt es zwei Möglichkeiten, eine Methode mit generischen Datentypen als Eingabe und Ausgabe zu definieren. Nehmen wir etwa das vorige Beispiel zum Sortieren eines Arrays und lassen es auf einer Liste (java.util.List<T>) operieren. Dann haben wir offensichtlich die folgenden beiden Möglichkeiten, die
Methode zu definieren:
2
// Sortierung als parametrisierte Methode:
static <T extends Comparable> List<T> sortList( List<T> list );
// Sortierung als regulaere Methode mit generischen Wildcard Typen:
static List<? extends Comparable>
sortList( List<? extends Comparable> list );
Welche Möglichkeit sollte man nun vorziehen? Eine Daumenregel lautet, man
sollte, wo möglich, auf generische Methoden verzichten und stattdessen Wildcards benutzen. Falls jedoch in der Implementierung der Methode der generische
3 Daher
wird der Basistyp dann auch als Erasure bezeichnet (engl. to erase=löschen).
5.1 Generische Datentypen
273
Typ T noch benötigt wird, um andere Abhängigkeiten etwa zu anderen Datentypen oder zum Rückgabedatentyp auszudrücken, sollte man eine generische Methode nutzen. Also sollte man hier eher die generische Methode wählen.
Im folgenden Beispiel aus der Klasse java.util.Collections nutzt man eher
die Wildcards, weil in der generischen Methode der Typparameter S nur als Hilfstyp deklariert und sonst nicht benötigt wird:
5
class Collections {
// Mit Wildcards:
public static <T> void
copy(List<T> dest, List<? extends T> src) {
...
}
// Mit generischer Methode:
public static <T, S extends T> void
copy(List<T> dest, List<S> src) {
...
}
10
}
Korrekt sind natürlich beide Deklarationen, die Abwägung ist von Fall zu Fall zu
entscheiden. Oftmals bietet jedoch die generische Methode eine bessere Lesbarkeit gegenüber komplexen Wildcard-Definitionen.
5.1.8 Interoperabilität mit vorhandenen Applikationen
Bei der Definition der Generizität in der Java-Version 5.0 wurde größter Wert auf
die Interoperabilität mit bestehendem Code gelegt. Denn die existierende Codebasis umfasst Millionen von Zeilen von Quelltext, und keinesfalls wird jemand
in der Lage sein, diese über Nacht auf die neue Sprachvariante umzusetzen, geschweige denn zu testen. Daher ist es ohne Probleme möglich, sowohl neu definierte Klassen mit Nutzung von parametrisierten Typen mit altem Code zu nutzen, als auch alten Code mit generischen Klassen nach und nach aufzubessern. In
diesem Abschnitt werden beide Richtungen aufgezeigt und diskutiert.
Nutzung von vorhandenen Bibliotheken in generischem Code
Nehmen wir an, die folgende Klasse HRDatabase wird als Bibliothek ausgeliefert
und soll in einem Projekt, in dem man nun die neuen generischen Erweiterungen nutzen kann, eingebunden werden. Generell wird hier die Einbindung von
Legacy-Bibliotheken in neuen Programmen diskutiert.
2
class HRDatabase {
List getAllNames()
{...}
}
274
5 Spracherweiterungen der Java Version 5.0
Wird die Klasse HRDatabase nun in neuem Code benutzt, wäre es schön, die
generische Typsicherheit nutzen zu können; immerhin wissen wir, dass die Methode getPersonsByBirthday eine Liste von Personen (List<Person>) liefert.
Das ist sehr einfach möglich, wie das folgende Beispiel zeigt:
1
HRDatabase db = HRDatabase.getInstance();
List<String> allNames = db.getAllNames();
Die letztere Zuweisung funktioniert tatsächlich so – ohne zusätzliche Typwandlung. Dies liegt daran, dass der bestehende Code den Ergebnistyp List hat, welcher den rohen Typ (Raw Type) der generischen Klasse List<T> darstellt. Zuweisungen zwischen rohen Typen und beliebigen generischen Instanziierungen
sind immer explizit erlaubt, um mit bestehenden Programmen interoperabel zu
bleiben. Da der Übersetzer jedoch nicht wissen kann, ob tatsächlich eine Liste von
Strings oder eine anders gefüllte Liste zurückkommt, wird bei allen Zuweisungen
von Raw Types grundsätzlich eine Unchecked Warning ausgegeben. An allen Stellen, an denen solch eine Warnung ausgegeben wird, ist potenziell später mit Laufzeitfehlern zu rechnen, wenn die erwarteten Typen nicht mit den tatsächlich zur
Laufzeit auftretenden übereinstimmen. In dieser Hinsicht sind Raw Types noch
freier als Wildcards nutzbar.
Nutzung von generischen Bibliotheken in vorhandenem Code
Nehmen wir nun den gegensätzlichen Fall an, dass die Klasse HRDatabase vom
Hersteller mit generischen Datentypen verfeinert wird. Es existiert nun noch Code, der die alte Schnittstelle nutzt und dies auch in Zukunft ohne Änderung tun
soll. Zunächst einmal die Veränderung in der Klasse HRDatabase:
3
class HRDatabase {
List<String> getAllNames()
{...}
}
Schauen wir uns nun ein Fragment in traditionellem Java an, das die Bibliothek
nutzt:
1
HRDatabase db = HRDatabase.getInstance();
List allNames = db.getAllNames();
Die Methode getAllNames selbst gibt nun eine typsichere Liste von Strings
zurück, die jedoch von der benutzenden Klasse in einem Raw Type abgelegt wird.
Dies ist ebenfalls wiederum ohne weitere Probleme möglich, und die Funktionalität wird in keinster Weise eingeschränkt oder semantisch geändert.
Die beiden kleinen Beispiele in beide Migrationsrichtungen zeigen, dass eine langsame Migration von bestehendem Code vollständig unterstützt wird. Bibliotheken können nach und nach mit generischen Datentypen verfeinert werden, ohne
dass bestehende Programme hierdurch benachteiligt werden; in den mitgelieferten Klassen, allen voran das Java Collections Framework, wurde dies schon rea-
5.1 Generische Datentypen
275
lisiert. Nutzer von Bibliotheken können schon bevor die Typsicherheit in die Bibliothek selbst integriert wird, auf ihrer eigenen Seite die Erweiterungen nutzen
und von höherer Produktivität profitieren.
Ein ausführlicheres Beispiel aus der Praxis
Bei der Verfeinerung des Java Collections Frameworks mussten die Autoren
darauf achten, dass die Signaturen der generischen Methoden nachher, mit
gelöschten Parametertypen, den originalen Signaturen entsprechen, damit kein
existierender Code unbrauchbar gemacht wird. Ein eindrucksvolles Beispiel
erläutert Gilad Bracha an der Methode Collections.max, die das größte Element in einer Collection zurückgibt. Die traditionelle Signatur dieser Methode ist
wie folgt definiert:
3
public class Collections {
public static Object max( Collection coll );
}
Diese Methode soll nun also mittels Generizität verfeinert und typsicher gemacht
werden. Zunächst einmal stellt man fest, dass man zur Feststellung des maximalen Elements vergleichen muss, dass also die eingegebenen Objekte die Schnittstelle Comparable implementieren müssen. Mit einiger Sicherheit wird der erste
Wurf der generischen Schnittstelle so aussehen:
2
public static <T extends Comparable<T>>
T max(Collection<T> coll)
Die Definition ist jedoch zu restriktiv. Warum? Weil eine Klasse K nicht unbedingt Comparable<K> implementieren muss, aber trotzdem, etwa durch
Comparable<Object>, vergleichbar ist und damit das Maximum auffindbar
wäre, die Methode sich nicht aufrufen ließe und einen Fehler liefern würde. Es
genügt also, wenn T die Schnittstelle Comparable für einen Supertyp implementiert. Dafür reicht die bisherige Ausdrucksstärke nicht aus, es gibt hierfür das
Schlüsselwort super, wie man anhand folgender Definition sehen kann:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
Diese Definition ist schon sehr gut, hat aber immer noch einen Schönheitsfehler. Die ursprüngliche, traditionelle Signatur der Methode lieferte den Typ Object zurück, doch der gelöschte Typ unserer Signatur ist Comparable; damit werden einige vorhandene Programme mit einem Laufzeitfehler die Arbeit verweigern. Jedoch was tun? Die Eigenschaft der Vergleichbarkeit ist ja nicht optional. Natürlich hätte man in der ursprünglichen Definition der Methode schon
Comparable zurückliefern können, aber es ist nun einmal nicht so definiert worden und nun auch nicht mehr änderbar.
Speziell für diesen Zweck kann man den gelöschten Typ, der für die tatsächliche
interne Implementierung benutzt wird, erzwingen. Das Beispiel für die Metho-
276
5 Spracherweiterungen der Java Version 5.0
de max mit generischen Typen und vollständig kompatibel zur ursprünglichen
Methode sieht nun so aus:
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
Mit dem Und-Zeichen getrennt, kann man mehrere Bounds für den Parametertyp
angeben, der jeweils erste wird immer für die tatsächliche Implementierung, also
für die interne Generierung des gelöschten Typs, verwendet.
5.1.9 Zusammenfassung
Wir haben die Spracherweiterungen zur Unterstützung von generischen Datentypen eingeführt und sie als nützliches Hilfsmittel gefunden. Insbesondere im
Einsatz von polymorphen Algorithmen kann nun, im Gegensatz zur bisherigen
Java-Version, bereits zur Übersetzungszeit Typsicherheit gewährt werden. Somit
können viele potenzielle Laufzeitfehler schon vorher eliminiert und wesentlich
robustere Programme entwickelt werden. Die Ausdrucksstärke der generischen
Typen, insbesondere mit Wildcard und der Angabe von Upper Bounds, reicht für
alle praxisrelevanten Fälle aus. In Kapitel 6.1 werden wir sehen, wie mit diesen Erweiterungen das Java Collections Framework noch robuster und eleganter wird.
5.2 Vereinfachte Schleifen (foreach)
Eine weitere Neuerung in Java 5.0 sind die vereinfachten Schleifen [Sun04d]. Wie
schon im Abschnitt 3.4.2 angesprochen wurde, werden viele Schleifen dazu benutzt, um die Elemente einer Liste (Array) oder einer Sammlung (Collection) der
Reihe nach zu bearbeiten. Der typische Aufbau einer solchen Schleife hat in älteren Java-Versionen folgendes Aussehen:
// Array initialisieren
int[] Daten = {5,4,3,2,1,0};
3
// die Daten der Reihe nach auf den Bildschirm ausgeben
for(int i = 0; i < Daten.length; i++)
System.out.println(Daten[i]);
8
13
// Eine Liste erzeugen
ArrayList list = new ArrayList();
// Elemente zur Liste hinzufügen
list.add(new Integer(5));
list.add(new Integer(4));
list.add(new Integer(3));
list.add(new Integer(2));
list.add(new Integer(1));