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