Montag, 21. September 2009

Generics: Generics auf Klassenebene

Zuletzt war das Thema, wozu man Generics zum Beispiel brauchen kann. In dem Beispiel gab es eine Methode, die einen generischen Typen verwendet hat, damit der Parameter mit dem tatsächlichen Typen zurückgegeben werden kann.
Methoden sind aber nur ein Ort, wo man Generics verwenden kann. Der zweite sind ganze Klassen. Dazu mache ich kein eigenes Beispiel, sondern verwende ein bestehendes: ArrayList. Beim Lernen von Collections tauchen vielleicht solche Sachen auf:

ArrayList l = new ArrayList();
l.add("a");
l.add("bb");
Iterator it = l.iterator();
while(it.hasNext()) {
System.out.println(((String) it.next()).length());
}

Die Liste enthält nur Strings, und soll auch nur Strings enthalten, aber der Compiler weiß es nicht. Das riecht schwer nach dem Einsatzgebiet für Generics!

ArrayList<String> l = new ArrayList<String>();
l.add("a");
l.add("bb");
Iterator<String> it = l.iterator();
while(it.hasNext()) {
System.out.println(it.next().length());
}

Der Klassenkopf von ArrayList sieht so aus:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable

Der Klassenkopf enthält die Typenvariable E dreimal: einmal hinter dem Klassenname, zweimal im extends/implements. Der Wert der Typenvariable wird beim Konstruktoraufruf festgelegt: new ArrayList<String>(); Dieser generische Typ verleiht dem ganzen eine gewisse Typensicherheit:

ArrayList<String> l;
l = new ArrayList<Date>(); //funktioniert nicht
l = new ArrayList<String>(); //funktioniert

//Der rückgabetyp von ArrayList<E>.iterator() ist vom Typ Iterator<E> (also hier Iterator<String>)
Iterator<Date> it = l.iterator(); //funktioniert nicht
Iterator<String> it = l.iterator(); //funktioniert

while(it.hasNext()) {
//Ebenso liefert Iterator<E>.next() ein E (hier einen String) zurück
Date d = it.next(); //funktioniert nicht
String s = it.next(); //funktioniert
}

ArrayList<Object> l2 = l; //funktioniert auch nicht!
List<String> l2 = l; //funktioniert schon

Alle diese Fehler werden schon vom Compiler erkannt! Im letzten Punkt steckt eine Rafinesse. Warum ist ArrayList mit List, aber nicht String mit Object kompatibel? Die Antwort gibts nächstes mal!

Sonntag, 20. September 2009

Generics: Erste Schritte

Wenn man Gerüchte über Generics hört, dann heißt es meistens, dass es eine ganz komplizierte Angelegenheit ist. Ich weiß gar nicht, wer auf die Idee gekommen ist.
Bei Generics geht es darum, Einen Klassennamen durch eine Variable zu ersetzen. Wer sich darunter schon etwas vorstellen kann, gut. Ansonsten macht es auch nichts.
Stell dir folgende Situation vor: Du willst die Objekte speichern, die im Laufe deines Programms erzeugt werden:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public void addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

//size++ wird hier als Ausdruck und nicht als Anweisung benutzt.
//Der Wert des Ausdrucks ist der alte Wert
//Auf ähnliche Weise ist ++size der Wert nach der Erhöhung
objects[size++] = o;
}

//hier gleich ein Kleiner Test
//Die Objekte werden danach noch verwendet, deswegen werden sie in Variablen gespeichert
public static void main(String[] args) {
ObjectList l = new ObjectList();

String s = "hallo";
l.addObject(s);
System.out.println(s.length());

Date d = new Date();
l.addObject(d);
System.out.println(d.getTime());
}
}

Vielleicht findest du die Main-Methode genau so mühsam wie ich. Netto will ich ja von beiden Objekten nur eine Sache: einmal die Länge, und einmal die Millisekunden. Dazu würde auch "hallo".length() oder new Date().getTime() genügen. Eine lokale Variable wäre unnötig.
Und was kann man dafür tun? Zum Beispiel den Parameter in der gleichen Methode wieder zurückgeben:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(((String) l.addObject("hallo")).length());
System.out.println(((Date) l.addObject(new Date())).getTime());
}
}

Das ist ehrlich gesagt auch nicht meines. Ich weiß ja eigentlich ganz genau, was raus kommt, nur der Compiler weiß es nicht.
Wie bringt man es dem Compiler bei? Da gibt es natürlich den hässlichen Weg: Die Methode für die relevanten Typen überladen...


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public Object addObject(Object o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public String addObject(String s) {
return (String) addObject((Object) s);
}

public Date addObject(Date d) {
return (Date) addObject((Object) d);
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

Was ein relevanter Typ ist, kann sich ja ändern. Und deshalb gibt es, angefangen mit Java 5, dieses schöne neue Feature Namens Generics.
Wenn man Generics benutzt, wird der Typ sozusagen nicht in der Methodendeklaration sondern beim Aufruf deklariert. (Dabei meine ich allerdings nicht, während das Programm läuft, sondern wenn der Aufruf kompiliert wird. Die Feinheiten gibts vielleicht in einem späteren Post!)
Hier erst einmal der schöne Code:


package post09;


import java.util.Date;


public class ObjectList {
private Object[] objects = new Object[10];
private int size = 0;

public <T> T addObject(T o) {
if(objects.length == size) {
Object[] objects = new Object[this.objects.length];
System.arraycopy(this.objects, 0, objects, 0, size);
this.objects = objects;
}

objects[size++] = o;
return o;
}

public static void main(String[] args) {
ObjectList l = new ObjectList();

System.out.println(l.addObject("hallo").length());
System.out.println(l.addObject(new Date()).getTime());
}
}

T ist hierbei eine Typenvariable und wird in spitzen Klammern deklariert. Der "Wert" der Variable kommt beim Aufruf zustande, indem der Typ des Parameters genommen wird. In weiterer Folge hat T in der ganzen Methode den gleichen Wert und kann ohne Casts verwendet werden.
Das heißt, wenn addObject mit einem String als Parameter aufgerufen wird, liefert es auch einen String zurück.