Advanced   Java   Services Generics in Collections


Warum Generics

Im folgenden das klassische Problem, das zur Einführung von Generics geführt hat. Wir legen eine ArrayList an und nehmen Objekte auf. In Zukunft werden wir das eine ungesicherte ArrayList nennen. Natürlich können wir auch irgendeine andere Containerklasse aus der Collectionhierarchie wählen. Alle Objekte dieser Klassen sind so konzipiert, daß sie beliebige Javaobjekte aufnehmen können.

ArrayList list = new ArrayList();   //1
list.add("Hello World");            //2
list.add(new Integer("1"));         //3
String hi = (String)list.get(0);    //4
String low = (String)list.get(1);   //5  RuntimeException: ClassCastException

Wenn wir annehmen, daß die ArrayList zum Aufnehmen von Strings eingeführt wurde, so ist in Zeile 3 ein Fehler passiert. Beim Zugriff auf die enthaltenen Elemente mit get() hat diese Methode den Returntyp Objekt, da man beliebige Objekte aufnehmen kann. Nach unserer Information enthä die Liste aber nur Strings, deswegen casten wir alles von Object auf String. Der Compiler kann den Fehler in Zeile 5 nicht feststellen und es kommt zur Laufzeit zu einer ClassCastException. Die in der Version 1.5 eingeführten Generics ermöglichen es dem Compiler, diesen Fehler zu entdecken und somit wird aus einer RuntimeException ein Compilerfehler.

Vorteil: Generics verwandeln RuntimeExceptions zu Compilerfehlern

Wir verwenden hier als Klasse aus der Collectionhierarchie ArrayList, die Beispiele sind aber auch für andere Klassen dieser Hierarchie anwendbar.


Einfache Beispiele
ArrayList<String> arrayList = new ArrayList<String>();  // (1) Java 5 und 6
ArrayList<String> stringList = new ArrayList<>();  // (2) ab Java 7 lediglich spitze Klammern
ArrayList<Number> numberList = new ArrayList<>();  // (3)
ArrayList<Integer> integerList = new ArrayList<>();  // (4)

Durch die Typangabe in den spitzen Klammern erhält die ArrayList den generischen Typ String und ist ausschließlich für Strings zuständig. Beim Aufnehmen der Objekte kann der Compiler nun den Typ prüfen. Beim Anwenden der get()-Methode ist kein Cast mehr notwendig, der Compiler fügt ihn für uns ein.

ArrayList<String> stringList = new ArrayList<String>();
arrayList.add("Generics sind eine gute Sache");
String s = arrayList.get(0);

Ähnlich wie bei Arrays kann man auch Objekte eines Unterklassentyps aufnehmen (Polymorphie!).

ArrayList<Number> arrayList = new ArrayList<>();
arrayList.add( new Integer(17) );
arrayList.add( new Double(3.14) );

Und wieder rausholen

Number num1 = numberList.get(0);

Number num1 = numberList.get(1);

Da die ArrayList den generischen Typ Number hat, liefert get() Elemente vom Typ Number. Ob diese Elemente Integer oder Double usw. muß dann noch untersucht werden.


Primitive Datentypen und Autoboxing bzw. Unboxing

Generische Typen können nur Klassen und Interfaces sein, keine primitiven Datentypen.

Richtig: ArrayList<Integer> integerList = new ArrayList<>();
Falsch:  ArrayList<int> integerList = new ArrayList<>();

Will man primitive Daten aufnehmen, so muß man sie in einem Wrapperklassenobjekt verstecken:

ArrayList<Integer> integerList = new ArrayList<>();

arrayList.add( new Integer(17));

Autoboxing vereinfacht die Aufnahme (und verschleiert den tatsächlichen Vorgang...):

ArrayList<Integer> integerList = new ArrayList<>();

arrayList.add(17);

Auch beim Zugriff auf die so gespeicherten Elemente muß man wieder über Hüllobjekte gehen.

Integer wrapped = integerList.get(0);
int unWrapped = wrapped.intValue();

Auto-Unboxing vereinfacht das Rausholen (und verschleiert den tatsächlichen Vorgang...):

int i = integerList.get(0);

Keine Typverwandtschaft

Durch die Spezialisierung der Collectionklassen mit Hilfe von generischen Typen entstehen keine neuen Datentypen. So ist etwa ArrayList<Number> nicht typverwandt mit ArrayList<Integer>

Falsch:

ArrayList<Integer> integerList = new ArrayList<>();
ArrayList<Number> numberList = integerList; //  incompatible types

In eine Liste mit dem generischen Typ Number kann man beliebige Numberobjekte auf nehmen, also etwa auch Double oder Float. Deshalb darf die Referenz numberList nicht auf eine Referenz integerList verweisen in die man ausschließlich Integer aufnehmen kann.


Type Erasure

Generische Typen existieren nur im Quellcode und ermöglichen dem Compiler Fehler beim Aufnehmen und Rausholen von Objekten zu entdecken. Wird der Code compiliert wird die Typinformation gelöscht und der Compiler fügt beim Rausholen der Elemente wieder einen Typecast ein. Nur dadurch konnte Kompatibilität zu altem Javacode hergestellt werden. Konsequenz ist, daß ein generischer Typ kein echter Typ ist. Nach dem Compilieren wird ArrayList<Integer> myList einfach wieder zu ArrayList myList. Ebenso wird etwa ArrayList<String> auch zu ArrayList. Mit diesem Konzept erreichte man die Kompatibilität zu altem Javacode der noch keine Generics kennt. Eine andere Implementierung der Generics war also nicht möglich. Dadurch wird das Regelwerk zu Generics etwas aufwendiger.


Generische Collections in Methodensignaturen

Unmittelbare Konsequenz von Type Erasure ist, daß der Compiler Signaturen, die sich nur im generischen Typ unterscheiden nicht akzeptieren kann.

So sind beispielsweise die folgenden beiden Signaturen nicht zulässig.

Falsch:

void method(ArrayList<Integer> list)
{
}

void method(ArrayList<Byte> list)
{
}

error: name clash: method(ArrayList<Byte>) and method(ArrayList<Integer>) have the same erasure


Generische Typen mit dem Fragezeichenplatzhalter

Wir betrachten folgende an sich einfache Situation, die der Compiler klaglos akzeptiert.

void method(ArrayList<Number> numberList)
{
   System.out.println(numberList.get(0));
   numberList.add(12.34); // (*)
}

Dazu paßt folgender Aufruf.

ArrayList<Number> numberList = new ArrayList<>();
numberList.add(3.14);
numberList.add(17);
method(numberList);

Obwohl bereits indirekt angesprochen kann man auf folgende Idee kommen...

ArrayList<Integer> intList = new ArrayList<>();
numberList.add(3);
numberList.add(17);
method(intList);

...die der Compiler aber nicht mitmacht.

error: method in class Test cannot be applied to given types;
required: ArrayList<Number>  found: ArrayList<Double>
reason: actual argument ArrayList<Double> cannot be converted to ArrayList<Number> by method invocation conversion

Wäre dies erlaubt, so würde man in (*) ein Double in eine Liste aufnehmen, die nur für Integer zuständig ist. Würde man eine solche Liste nur lesen, kann man allerdings keinen Schaden anrichten.


Lizenz zum Lesen mit der Syntax <? extends BaseClassOrInterface>

Mit der oben eingeführten Syntax ist es möglich bei der Parameterübergabe flexibel zu werden. So bedeutet die folgende Signatur

method(ArrayList<? extends Number> numberList)

Damit sind die beiden folgenden Aufrufe legal.

ArrayList<Integer> intList = new ArrayList<>();
method(intList);
ArrayList<Double> doubleList = new ArrayList<>();
method(doubleList);

Innerhalb der Methode ist nun folgendes zuläässig.

method(ArrayList<? extends Number> numberList)
{
   for(Number number : numberList)
   {
      System.out.println(number);
   }
}

Welche Beziehung muß eine Klasse Complex erfüllen, damit folgende Situation legal ist?

ArrayList<Complex> compList = new ArrayList<>();
method(compList);

Lizenz zum Schreiben mit der Syntax <? super Child>

Mit einiger Vorsicht kann man diese Idee auf Schreibvorgänge übertragen. Man kann schon erwarten, daß in diesem Fall ArrayListen übergeben werden können, deren generischer Typ eine Elternklasse von Child ist.

ArrayList<Integer> intList = new ArrayList<>();
method(intList);
ArrayList<Double> doubleList = new ArrayList<>();
method(doubleList);

Wenn obige Aufrufe erlaubt sein sollen, dann müßte Child sowohl Integer als auch Double als Eltern haben. Eine solche Klasse gibt es aber nicht und wird es nie geben, da Double und Integer final sind. Wenn wir aber etwa Integer als Child nehmen, können wir zumindest Arraylisten mit dem generischen Typ Number aufnehmen.

ArrayList<Integer> intList = new ArrayList<>();
method(intList);
ArrayList<Number> numberList = new ArrayList<>();
method(doubleList);

Eine passende Methodensignatur sieht dann so aus.

method(ArrayList<? super Integer> list)

Hier ist der Supertyp von Integer (dem Child) nicht bekannt. Da es aber ein Supertyp ist, kann man nichts falsch machen, wenn man Objekte vom Typ Child aufnimmt. Dies ist denn auch der Methode erlaubt. Auch Lesen ist erlaubt, da man aber den Supertyp nicht kennt muß man Object als Supertyp nehmen.

method(ArrayList<? super Integer> list)
{
   list.add(17);  // Integer kann man aufnehmen !
   // Lesen geht nur mit Objekt
   for(Object ob : list)
   {
      System.out.println(ob);
   }
}

Kleine Ergänzungen