Home Unternehmen             Portfolio             Trainingsshop    Kunden & Partner Aktuelles    Kontakt
Home  Pfeil  ORDIX News  Pfeil  2/2009  Pfeil  Java/JEE
suche: 
Dieser Artikel richtet sich an Java-Entwickler und Architekten, die einen Einblick in das Konzept von Java Generics bekommen möchten.

Glossar

Generics
In Java gibt es mit Generics die Möglichkeit, parametrisierte Klassen zu benutzen. Es war die bedeutendste Spracherweiterung in der Java Version 5.
JSR
Java Specification Request. JSR kennzeichnet eine Anforderung für eine Änderung der Programmiersprache Java, die vom Standardisierungskomitee, dem Java Community Process, eingebracht wird.
JVM
Java Virtual Machine. Programm, in dem Java-Programme ausgeführt werden. Eine JVM ist vom Betriebssystem abhängig, während das Java-Programm unabhängig vom Betriebssystem ist.
Mixin
"Ein Mixin verbindet (mixt) generell nutzbare Funktionalität (Methoden) im Code von vorhandenen Klassen zu einer erweiterten Funktionalität". Quelle: [2]
raw type
"Nackter" Typ. Das heißt, zu dem Typ gibt es keine Zusatzinformationen in Form von Parametern mehr. Aus parametrisierten Typen im Quellcode werden raw types im compilierten Code.

Weiterführende Links




Java Generics - Ein Segen, der nicht zum Fluch werden darf


Darauf hatte die Java Community lange gewartet. Mit dem Einzug von Generics in Java 5 im Jahre 2004 wurde die Tür zur "Meta-Programmierung" geöffnet. Seitdem lassen sich Java-Programme mittels dieser mächtigen Spracherweiterung eleganter und vor allem weitaus sicherer schreiben. Der erfahrene Java-Entwickler wird in die Lage versetzt, größere Komplexitäten zu meistern. Allerdings gibt es auch kritische Stimmen, die die Umsetzung des generischen Prinzips in Java für nicht gut gelungen halten. Was es mit der Kritik auf sich hat, welche Fallstricke lauern und wie man diese geschickt umgehen kann, wird in diesem Artikel anschaulich vorgestellt.

public class SomeClass {
  private String¹ memberVar;
  public Double² someMethode (Integer³ arg1){
    Byte⁴ localVar;
Abb. 1: Typangaben im Java Quellcode - alles muss ausgewiesen werden.
 
1 public class Kaefig <T extends Tier> {
2   private T armesTier;
3   public T Kaefig (String name) { ...
4   public T befreieTier() { ...
Abb. 2: Definition einer simplen, generischen Klasse.
 
Kaefig <Eelefant> kE = new Kaefig <Elefant> ("Jumbo");
Kaefig <Wellensittich> kE = new Kaefig <Wellensittich>
  ("Piepmann");
Abb. 3: Nutzung einer generischen Klasse, Instantiierung durch konkrete Typparameter.
 
// geeignet gefüllte ArrayList
ArrayList<Kontobewegung> alK ...
for (Kontobewegung k : alK ){
    k.drucke();
}
// geeignet gefüllte ArrayList
ArrayList alK ...
Kontobewegung k;
Iterator iK = alK.iterator();
while (ilK.hasNext()) {
    k=(Kontobewegung)ilK.next();
    k.drucke();
}
Abb. 4: Java 5 Quellcode mit Generics (links) und "alte" Vorgehensweise (rechts): Iterator holen und initialisieren, Collection "durchhecheln" und jedes Mal ein "Cast". (Und hoffen, dass auch nichts schiefgeht.)
 

Einführung

Das Thema "Java Generics" ist nicht taufrisch, denn schließlich startete die Diskussion um die Hinzunahme von "generischen Möglichkeiten" in Java schon im Jahre 1999 mit dem JSR 14 (JSR-014: Add Generic Types To The Java Programming Language). In der objektorientierten Softwareentwicklung steht man gelegentlich vor der Herausforderung, innerhalb von Klassen in den Programmzeilen der Geschäftslogik mit Objekten unbekannten Typs umgehen zu müssen. Das kommt z. B. in Kontexten vor, in denen es um die Verwaltung von Geschäftsobjekten geht. Die Verwaltung ist hier im Sinne von IT-gerechter Ablage und Strukturierung solcher Objekte gemeint. Dabei benötigt man ein programmiertechnisches Ausdrucksmittel, um konkrete Algorithmen schreiben zu können, die dann mit Objekten unbekannter Herkunft umgehen und diese ggf. auch manipulieren können.

Solche Aufgabenstellungen waren in früheren Java-Versionen (< Java 5) nicht sehr gut zu lösen, weil es das Konzept des "unbekannten Typs", der aber dennoch zur Laufzeit eines Programms feststeht und damit unveränderbar ist, nicht gab. Man findet als Ausdrucksmittel in anderen Programmierwelten häufig den Begriff der "generischen Datentypen". Dieses Manko in Java führte in der Community sehr früh zu dem Wunsch, ein ähnlich mächtiges Konzept zur Verfügung zu haben, wie man es z. B. bei den Templates in C++ findet. Diesen Wunsch hat der JSR 14 aufgegriffen und nach einem langen Spezifikationsprozess gilt das neue Konzept schließlich in der Version 5 von Java als die Hauptneuerung.

Was sind Generics in Java eigentlich, und was ist der Grund dafür, dass sie nicht ganz unumstritten sind? Das klassische Einsatzgebiet von Generics in Java, welches auch in unzähligen Tutorials und Beiträgen auftaucht, ist sicherlich das Collection Framework von Java, das hauptsächlich aus so genannten Containerklassen besteht, wie ArrayList, TreeSet oder HashMap. Diese Klassen haben hauptsächlich Verwaltungsaufgaben und unterscheiden sich lediglich darin, wie sie diese Verwaltung von Elementen durchführen.

Starke Typen

Der Begriff des Typs spielt sowohl in Java als auch beim Thema Generics eine zentrale Rolle, so dass wir den Typen im Folgenden etwas genauer beschreiben möchten.

Java ist bekanntlich eine streng typisierte Sprache, was nichts anderes heißt, als dass jedes Objekt, das im Java-Quellcode auftaucht, einen konkreten Typ besitzt. Und damit haben wir noch nicht alles erwischt, denn auch die primitiven Datentypen (int, boolean, byte etc.) repräsentieren einen Typen. Dieser Typ ist bei der Einführung einer Variablen, bei der Angabe eines Methodenparameters und eines Rückgabeobjektes nötig und ist somit immer explizit anzugeben, wenn ein neuer Bezeichner im Quellcode auftaucht.

An folgenden Stellen im Quellcode kann der Typ also stehen (siehe Abbildung 1):

1. Typ einer Member-Variablen
2. Typ einer lokalen Variablen
3. Typ eines formalen Methodenparameters
4. Typ des Rückgabeobjektes einer Methode

Typ unbekannt, scheint aber nett zu sein

Nun sehen wir uns an, wie ein generischer Typ aussieht -im Gegensatz zu "einfachen" Typen aus Abbildung 1 - und welche syntaktischen Hilfsmittel uns zur Verfügung stehen. Generics kommen zum Einsatz, wenn bei der Definition einer Klasse der Typ eines intern benötigten Objektes nicht bekannt ist, zur Entwicklungszeit also noch nicht feststeht.

Deshalb wird bei der Definition einer generischen Klasse eine "Typvariable" mit der Syntax <T...> eingeführt. Der Bezeichner T kann somit anstelle einer normalen Typangabe innerhalb der Klassengrenze verwendet werden, analog dazu, wie wir es vom Umgang mit normalen Variablen oder mit normalen Rückgabewerten von Methoden auch kennen, nur dass dann kein konkreter, sondern ein generischer Typ zu verwenden ist (siehe Abbildung 2). Mit dem Konstrukt <...extends Tier> wird ausgedrückt, dass der generische Typ T nicht völlig frei, sondern gebunden ist (so genannte Bounds). T muss eine direkte oder indirekte Ableitung von der Klasse Tier sein bzw. das Interface Tier implementieren.

Sehr wichtig für das generelle Verständnis von Generics ist es, an dieser Stelle schon einmal die Unterscheidung zu treffen zwischen

Die Einführung der Typvariablen steht nur einmal am Anfang bei der Klassendefinition, wenn es gilt, den Typen vorzustellen. Schließlich muss man wissen, mit wem man es zu tun hat. Des Weiteren kann der Bezeichner der Typvariablen, der nach der Konvention meistens nur aus einem groß geschriebenen Buchstaben besteht, an jeder Stelle stehen, wo auch ein normaler Typ vorkommen kann.

Instantiierung - vom Generalisten zum Spezialisten

Eine so gestaltete, generische Klasse kann nun genutzt werden, indem man sie konkret macht, was nichts anderes heißt, als sie mit einem aktuellen Typen zu instantiieren. Das Prinzip der Instantiierung kennen wir in Java ja schon. Instanzen von Klassen erzeugt man zur Laufzeit, indem man geeignete Konstruktoren mit aktuellen Parametern aufruft. Ganz analog dazu werden generische Klassen zu konkreten Klassen, indem wir sie mit aktuellen Typparametern versorgen.

Nehmen wir nun an, ich brauche einen konkreten Käfig für meinen Wellensittich und einen konkreten Käfig für meinen Elefanten (siehe Abbildung 3; Elefant und Wellensittich seien geeignete Klassen, die von einer Klasse Tier erben).

Positive Merkmale der Generics

Bevor es mit den Einschränkungen und Ungereimtheiten, die Generics mit sich bringen, weitergeht, soll hier zunächst herausgestellt werden, dass es sich um einen großen Fortschritt im Evolutionsprozess von Java handelt. Einige positive Merkmale der Generics werden im Folgenden genannt und sollen eine Vorstellung davon vermitteln, welchen großen Fortschritt es in die Welt der Java-Entwickler gebracht hat:

First-class Objects, eine ehrenwerte Gesellschaft

Des Weiteren werden einige etwas unschöne Aspekte der Generics aufgezeigt, die vor allem Sprachpuristen stark bemängeln, auf die aber auch schon viele Java-Entwickler gestoßen sein dürften. So ist es z. B. für viele sehr überraschend,dass es ein JavaArray von generischen Typen nicht gibt. Man kann also kein Array von Spezialkäfigen definieren, so dass der Java-Code Kaefig <Eelefant> kE[100] vom Compiler abgewiesen wird.

Der Grund für die vielen Ausnahmeregeln und Einschränkungen ist, dass man in dem Bestreben sucht, volle Kompatibilität zwischen altem und neuem Java zu schaffen. Neue Java 5 Programme sollten in Java 1.4 JVMs korrekt ablauffähig sein (forward compability). Umgekehrt sollten auch Java 1.4 Programme in neueren Java 5 JVM arbeiten können (backward compability). Der erste Punkt kann allerdings schon als gescheitert angesehen werden, wie entsprechende Tests mit dem jdk-1.5.03 beweisen.

Generische Klassen in Java werden unglücklicherweise nicht zu "first-class objects". Das bedeutet, dass es z. B. für zwei unterschiedlich instantiierte generische Klassen (siehe Abbildung 3) keine zwei .class-Dateien existieren und damit auch keine zwei .class-Repräsentationen vom Java Classloader geladen werden. Java Generics beruhen auf dem Prinzip des "Code Sharing" (im Gegensatz zu templates in C++), so dass alle unterschiedlichen Typen eines generischen Typs den gleichen compilierten Code verwenden. Diese Umsetzung des generischen Konzeptes in Java 5 wurde von Sun nicht nur aus Kompatibilitätsbetrachtungen vollzogen. Performance-Aspekte und auch die Größe von compilierten Java-Programmen spielten dabei ebenfalls eine große Rolle.

Umgesetzt wird das Prinzip der gemeinsamen Code-Nutzung durch das so genannte typ erasure. Das bedeutet, dass der Compiler alle generischen Informationen zu den Klassen entfernt und der compilierte Code nur noch "nackte" Typinformationen enthält. Man spricht in dem Zusammenhang vom raw type.

So nachvollziehbar diese Design-Entscheidung auch ist, so ist sie doch gleichzeitig die Quelle diverser Einschränkungen. Im Folgenden stellen wir eine kleine Auswahl vor:

  1. Static members - Typvariablen dürfen nicht in statischen Methoden (Klassenmethoden) auftauchen und auch nicht bei statischen Variablen (Klassenvariablen).

  2. Konstruktoren - Typvariablen dürfen nicht im Code oder in der Parameterliste von Konstruktoren erscheinen. Wie erwähnt, sind Konstruktoren einer Klasse spezielle Methoden, die nach der Klasse benannt sind und eine Instanz der Klasse erzeugen.

  3. Typvariable ist kein Basistyp - Eine Typvariable darf nicht als Basistyp verwendet werden. Demnach ist das Konstrukt class Verboten<T> extends T nicht zulässig.

  4. instanceof - Der Überprüfungsoperator instanceof und das Klassenliteral .class können nicht mit einer Typvariablen verwendet werden, d. h. instanceof T oder T.class gehen nicht.

  5. Instantiierung - Instanzen oder Arrays mittels new zu erzeugen, ist ebenfalls nicht 12 ORDIX News 2/2009 Java/JEE erlaubt, d. h. es darf kein new T() oder new T[] innerhalb einer generischen Klassendefinition verwendet werden.

  6. Arrays - Der Elementtyp eines Java Array darf kein generischer Typ sein. Auch das hängt sehr stark mit den raw types zusammen. Wäre es erlaubt, könnte über Zuweisungsketten ein Element mit unerlaubtem Typ in das Array gelangen, ohne dass es der Compiler überprüfen könnte.

  7. catch clause - Typvariablen dürfen nicht in den catch-Blöcken des Exception Handling innerhalb einer generischen Klasse verwendet werden.

Immer schön den Ball flachhalten

Die wichtigste Strategie im Umgang mit Generics ist, die oben beschriebenen Mankos und Unzulänglichkeiten sowie ihre Ursachen und Hintergründe zu kennen. Das erlaubt dem Entwickler, sie mit sauberem Klassendesign zu verhindern oder sie zumindest geschickt zu umgehen.

Es gibt zu einigen Punkten adäquate Umgehungsmöglichkeiten. Stellvertretend für diesen Umstand sehen wir uns den Punkt (3) aus der vorherigenAufzählung etwas genauer an. Denn das Konstrukt class Mixin<T> extends T, d. h. die Typvariable als Basistyp zu verwenden, ist in Entwicklerkreisen ein sehr begehrter Ansatz, den man in einschlägigen Foren auch unter dem Begriff Mixin finden kann.

Damit ließe sich das Decorator Pattern [1] gut umsetzen, da sich zu vorhandenen Klassen zusätzliche Methoden sehr effizient hinzufügen lassen. Aber leider können wir das aufgrund der besagten Einschränkung (noch) nicht nutzen. Die Alternative besteht in der Delegation, mit der das Decorator Pattern unter Nutzung bestehender Java-Sprachmittel vielfach umgesetzt ist, so z. B. bei den Streams im Java I/O.

Schlechter sieht es da bei Punkt (6) aus, bei dem aufgezeigt wird, dass die Elemente von Arrays keinen generischen Typ haben dürfen. Das ist häufig der Grund für Frustrationen und der Hinweis, die Klasse ArrayList an Stelle von Array zu verwenden. Dieses ist aber auch nur eine schwache Hilfe, denn vielfach hat man gar nicht die Wahl, eine solche Ersetzung vorzunehmen.

Fazit

Mit Generics hat ein sehr mächtiges Programmierkonzept Einzug in Java 5 gehalten, das die Programmierung mit generischen Typen erlaubt. Leider ist es durch die Art der Umsetzung mit raw types teilweise zu einer schwer verdaulichen Kost mit einer Reihe von Einschränkungen geworden. Ihre Kenntnis bewahrt vor allzu großen Hindernissen und lässt den Nutzen beim Einsatz dieses Sprachmittels deutlich überwiegen.

Auch den Verantwortlichen der Java-Sprache und der Java-Spracherweiterung sind diese Mankos sehr bewusst, so dass auch schon ein JSR "Refied Generics" unterwegs ist, den man in die kommende Version Java 8 unterzubringen versucht. Aktuell sieht es aber danach aus, dass die Überarbeitung der Generics später kommt. Trotz allem werden wir selbstverständlich am Ball bleiben und Sie darüber auf dem Laufenden halten.

Den Umgang mit Generics lernen und üben, können sie in unserem ORDIX Seminar "Java Programmierung Aufbau" [4].

Dr. Hubert Austermeier (info@ordix.de).