Generics 📌
Generics in Java ermöglichen es einer Klasse oder Methode mit Objekten arbiträrer Datentypen zu arbeiten und dennoch Typsicherheit (zur 👉 compile-time) zu gewährleisten. Das bringt es auf den Punkt, ist aber nicht gleich einleuchtend. Versuchen wir es mit einem Beispielszenario …
Das Problem
Angenommen, eine fiktive Datenstruktur BadList
hat die Aufgabe, Objekte in einer Art Liste zu verwalten.
Um neue Objekte in die Liste aufzunehmen, besitzt BadList
eine Methode add(Object o)
. Der Datentyp Object
ist hier deshalb gewählt, weil unsere BadList
natürlich nicht nur für Objekte eines einzigen Typs einsetzbar sein soll:
public class BadList {
// ...
public void add(Object o){
// ...
}
}
Wie genau die Liste intern funktioniert, soll uns hier egal sein. Aber der skizzierte Ansatz stellt uns bereits vor ein Problem: Wenn wir von dieser Liste später ein Objekt abrufen wollen - etwa mit einem Aufruf wie list.get(0)
- dann bekommen wir natürlich ein Object
zurück. Wir müssten dieses dann casten, um Zugriff auf die Methoden des konkreten Datentyps zu erhalten. Aber welcher ist das?
Jede Instanz unserer Liste sollte also nur Objekte eines bestimmten Datentyps aufnehmen können. Aber wie soll das gehen? Wenn wir statt Object
den konkreten Datentyp - z.B. Student
- einsetzen, funktioniert unsere Liste ausschließlich mit Student
-Objekten. Wir müssten für jeden zu verwaltenden Datentyp eine neue Liste programmieren.
Die Lösung: Generics
Hier am Beispiel generischer Klassendefinitionen
Eine Lösung für solche Situationen bieten Generics: Mit ihnen kann der Datentyp, mit dem eine Klasse oder Methode arbeiten soll, an mehreren Stellen im Code “gleichgeschaltet” werden, ohne diesen direkt festzulegen (eben ein generischer Typ). Dafür werden Platzhalter benutzt, die den später eingesetzten Datentyp markieren (hier T
):
public class GoodList<T> {
// ...
public void add(T o){
// ...
}
}
Die Syntax für die Definition des Platzhalters ist also <T>
(der Platzhalter in spitzen Klammern). In der restlichen Klasse (bzw. Methode, siehe unten!) ist T
dann als Typ verfügbar (siehe Parameter T o
in der Methode oben!).
Um diese Liste zu benutzen, wird nun bei ihrer Initialisierung der zu verwaltende Datentyp festgelegt:
GoodList<Student> students = new GoodList<Student>();
students.add(new Student()); // hinzufügen (Beispiel!)
Student s = students.get(0); // abrufen (Beispiel!)
Es muss hier keine Typumwandlung (casting) mehr stattfinden. Wir haben nun eine echte generische Datenstruktur, die für den Typen-Platzhalter T
den Typ Student
verwendet!
⚠️ Generics in Java funktionieren nur mit komplexen Datentypen!
Bei der Initialisierung der generischen Klasse kann der Datentyp auch weggelassen werden, wenn er in der Deklaration der zugehörigen Variable angegeben wurde (sog. diamond operator):
GoodList<Student> students = new GoodList<>();
Mehrere Typen-Platzhalter können durch Kommata getrennt definiert werden:
public class Something<X, Y> {
// ...
}
Natürlich sollte gut dokumentiert sein, wofür welcher Typ genutzt wird.
💬 Wann nutze ich welchen Platzhalter? Hier eine Antwort!
Type Erasure
Generics existieren nur zur compile time, d.h. vor dem Kompilieren des Codes. Dadurch kann der 👉 Compiler auf einen falsch verwendeten Datentyp (Teacher
-Objekt wird einer Liste für Student
-Objekte hinzugefügt o.ä.) bei der Kompilierung des Programmes reagieren und einen Fehler erzeugen. Nach der Kompilierung steht die Information des festgelegten Datentyps allerdings nicht mehr zu Verfügung, weil dieser währenddessen durch ein Verfahren namens type erasure entfernt wurde.
Unsere Liste aus dem Beispiel oben verwaltet nach der Kompilierung also tatsächlich wieder Object
-Instanzen!
Type Wildcards
Um Spielraum für den verwendeten Datentyp zu ermöglichen, kann ?
als Wildcard benutzt werden. Weil diese völlige Freiheit dann aber auch wieder völlige Unklarheit über den Datentyp bedeuten würde, wird der Typ üblicherweise nach oben oder unten begrenzt, d.h. es wird festgelegt, dass der Datentyp entweder eine bestimmte Klasse erweitern muss (extends
) oder eben eine Superklasse von einer bestimmten Klasse sein muss (super
). Die Syntax hierfür ist dann …
public class Something<? extends Foo> {
// ...
}
… bzw. …
public class Something<? super Foo> {
// ...
}
Generische Methodendefinitionen
Es ist außerdem möglich, die Typen von Rückgaben und Parametern von Methoden generisch zu gestalten:
public <T> Whoop<T, T> bar(T param) {
return new Whoop<T, T>(param, param);
}
Hier stellt die erste Erwähnung von T
, nämlich <T>
, die deklaration dieses Platzhalters dar. Alle weiteren Verwendungen von T
sind dann tatsächliche Platzhalter. Dies führt zu folgendem Verhalten (ausgehend vom Beispiel oben):
Yeah y = new Yeah();
foo.bar(y); // Rückgabe ist ein Whoop<Yeah, Yeah>
🔗 Siehe auch hier.
🔗 Java Generics FAQ