Generische Programmierung in Java
Generische Typen in Java
Ab Version 5.0 ("Tiger", 2004 veröffentlicht) stehen generische Typen auch in der Programmiersprache Java zur Verfügung. Sie werden hier Generics genannt und dienen dazu, Klassen mit zu verwendenden Typen zu parametrieren. Damit werden der Sprache Möglichkeiten ähnlich zu Templates in C++ eröffnet.
Beispiel in Java:
Ein Programm verwendet eine ArrayList
, um eine Liste von JButton
s zu speichern.
Bisher war die ArrayList auf den Typ Object fixiert:
ArrayList al = new ArrayList(); al.add(new JButton("Button 1")); al.add(new JButton("Button 2")); al.add(new JButton("Button 3")); al.add(new JButton("Button 4")); al.add(new JButton("Button 5")); for (int i = 0; i < al.length; i++) { JButton button = (JButton)al.get(i); button.setBackground(Color.white); }
Man beachte die notwendige explizite Typumwandlung (auch "Cast" genannt) sowie die Typunsicherheit, die damit verbunden ist. Man könnte versehentlich ein Objekt in der ArrayList
speichern, das keine Instanz der Klasse JButton
ist. Die Information über den genauen Typ geht beim Einfügen in die Liste verloren, der Compiler kann also nicht verhindern, dass zur Laufzeit bei der expliziten Typumwandlung von JButton
eine ClassCastException
auftritt.
Mit generischen Typen ist in Java Folgendes möglich:
ArrayList<JButton> al = new ArrayList<JButton>(); al.add(new JButton("Button 1")); al.add(new JButton("Button 2")); al.add(new JButton("Button 3")); al.add(new JButton("Button 4")); al.add(new JButton("Button 5")); for (int i = 0; i < al.length; i++) { al.get(i).setBackground(Color.white); }
Beim Auslesen ist nun keine explizite Typumwandlung mehr notwendig, beim Speichern ist es nur noch möglich, JButtons in der ArrayList
al abzulegen.
Interessant ist dann auch noch die Kombination von generischen Typen mit den erweiterten For-Schleifen. Obiges Beispiel lässt sich somit kurz fassen:
ArrayList<JButton> al = new ArrayList<JButton>(); al.add(new JButton("Button 1")); al.add(new JButton("Button 2")); al.add(new JButton("Button 3")); al.add(new JButton("Button 4")); al.add(new JButton("Button 5")); for (JButton b : al) { b.setBackground(Color.white); }
Varianzfälle
In Java können folgende Fälle unterschieden werden:
Invarianz: Bei Invarianz ist der Typparameter eindeutig. Invarianz bietet die größtmögliche Freiheit bei der Benutzung des Typparameters. Beispielsweise ist für Elemente einer ArrayList<Integer>, die alle vom Typ Integer oder abgeleitet sind, alles erlaubt, was für ein Integer eben erlaubt ist. Beispiel:
ArrayList<Integer> list = new ArrayList<Integer>(); ... Integer x = list.get(index); list.get(index).methodeVonInteger(); list.set(index, new Integer(98347));
Diese Möglichkeiten werden mit wenig Flexibilität bei der Zuweisung von Objekten der Generischen Klasse selber erkauft. Beispielsweise ist folgendes nicht erlaubt:
ArrayList<Number> list = new ArrayList<Integer>();
und das, obwohl Integer von Number abgeleitet ist. Der Grund liegt darin, dass der Compiler hier nicht mehr sicherstellen kann, dass zu dieser Liste stets der passende Typ hinzugefügt wird. Mit Arrays ist dies möglich, was eine Lücke im statischen Typsystem darstellt:
Number[] array = new Integer[10]; array[0] = new Double(5.0); // ArrayStoreException zur Laufzeit
Covarianz:
Covarianz ist mit generischen Typen dennoch möglich. Zunächst sollte allerdings der Begriff geklärt werden. Das nachfolgend beschriebene Verhalten heißt Covarianz, weil sich der Generische Typ in der Vererbungshierarchie genauso verhält wie der Typparameter selber. Mit anderen Worten: Ein Liste mit Elementtyp 'Integer' ist abgeleitet und Zuweisungskompatibel zu einer Liste mit Elementtyp 'Number', so wie 'Integer' von 'Number' abgeleitet ist. Dies entspricht dem Verhalten von Arrays.
Referenzen müssen explizit als covariant gekennzeichnet werden:
ArrayList<? extends Number> list; list = new ArrayList<Integer>(); list = new ArrayList<Double>(); list = new ArrayList<Long>();
Das Ablegen von Elementen in diese Listen ist nicht möglich, da, wie oben beschrieben, nicht typsicher. Möglich dagegen ist das Auslesen von Elementen:
Number x = list.get(index); // OK Integer x = list.get(index); // Fehler: Es muss sich bei // '? extends Number' nicht // um ein Integer handeln.
Generics bieten also wie Arrays covariantes Verhalten, verbieten aber alle Operationen, die typunsicher sind.
Contravarianz:
Contravarianz bezeichnet das Verhalten des Generischen Typs entgegen der Hierarchie des Typparameters. Übertragen auf das obige Beispiel würde das bedeuten: Eine Liste<Number> wäre "abgeleitet" (zuweisungskompatibel) von einer Liste<Double>! Funktionieren tut das folgendermaßen:
ArrayList<? super Double> list; list = new ArrayList<Number>(); list = new ArrayList<Double>(); list = new ArrayList<Object>();
Ein Objekt, das sich contravariant verhält, darf keine Annahmen darüber machen, inwiefern ein Element vom Typ U von T abgeleitet ist, wobei T der lower Typebound ist (im Beispiel von '? super Double' ist T 'Double'). Deshalb kann aus den obigen Listen nicht gelesen werden:
Number x = list.get(index); // Fehler: 'list' könnte vom Typ List<Object> sein Double x = list.get(index); // Fehler: 'list' könnte List<Object> oder List<Number> sein Object x = list.get(index); // Die einzige Ausnahme: Objects sind auf jeden Fall in der Liste
Unschwer zu erraten: Im Gegenzug kann in eine solche Liste ein Element abgelegt werden:
ArrayList<? super Number> list; list.add(myDouble); // OK: 'list' hat immer den Typ List<Number> oder List<Basisklasse von Number>. Damit ist die Zuweisung Double -> T immer erlaubt.
Bivarianz:
Zu guter Letzt bieten Generics noch bivariantes Verhalten an. Hierbei kann keinerlei Aussage über die Typparameter gemacht werden, denn es wird in beide Richtungen keine Grenze angegeben.
ArrayList<?> list; list = new ArrayList<Integer>(); list = new ArrayList<Object>(); list = new ArrayList<String>(); ...
Der Typparameter selber kann hierbei nicht genutzt werden, da keine Aussage möglich ist. Lediglich die Zuweisung T -> Object ist erlaubt, da T auf jeden Fall ein Object ist.
Nützlich kann sowas sein, wenn man nur mit dem generischen Typ arbeitet:
// Keine Informationen über den Typparameter nötig, // kann beliebige Listen aufnehmen. int readSize(List<?> list) { return list.size(); }