Generics und Klassen mit vielen Gesichtern
Java kennt keine Mehrfachvererbung, eine Klasse kann immer nur von einer Klasse abgeleitet werden. Das alternative Konzept sind Interfaces: Jede Klasse kann – zusätzlich zu ihrer Superklasse – beliebig viele Interfaces implementieren. Auf diese Weise kann ein Objekt einer Klasse – je nach Kontext – unterschiedliche Schnittstellen haben und unterschiedlich behandelt werden. Wie aber stellt man es an, ein Objekt gleichzeitig unter mehreren Blickwinkeln zu betrachten? Zur Motivation und zur Erklärung dieser zunächst etwas akademisch anmutenden Frage diene uns ein einfaches Beispiel. Angenommen, wir haben zunächst ein Interface das das Volumen von beliebigen Behältern beschreibt:
interface Behaelter { double volumen(); }
Dazu haben wir ein Interface, über das wir Informationen zum Inhalt abfragen können – zum Beispiel das spezifische Gewicht:
interface Inhalt { double dichte(); }
Mit diesen beiden Interfaces können wir nun – zum Beispiel um ein Schiff zu beladen – verschiedene Behälter definieren, die unterschiedliche Dinge enthalten können; über die fehlenden Maßeinheiten sehen wir mal großzügig hinweg. Von der weiteren Natur dieser Behälter sind unsere Interfaces völlig unabhängig:
public class Zuckersack implements Behaelter, Inhalt { @Override public double volumen() { return 100; } @Override public double dichte() { return 1.6; } }
Die Aufgabe besteht nun darin, eine Rechner-Klasse zu bauen die das Gewicht gefüllter Gefäße berechnet. Eine ebenso billige wie häßliche Methode sähe etwa so aus:
public double gewicht(Object dings) { if (dings instanceof Behaelter && dings instanceof Inhalt) { return ((Behaelter) dings).volumen() * ((Inhalt) dings).dichte(); } throw new IllegalArgumentException(); }
Abgesehen von der mangelnden Übersichtlichkeit fliegt uns das Konstrukt erst bei falscher Anwendung zur Laufzeit um die Ohren. Besser wäre es, wenn wir schon zur Compile-Zeit erführen, ob ein Objekt als Methoden- Argument geeignet ist oder nicht. Dank Javas statischer Typisierung sollte das eigentlich kein Problem sein und seit Java 1.5 kann man das mit Generics auch sehr viel hübscher hinbekommen. Eine generische Methoden-Signatur kann in Java zum Beispiel so aussehen:
public <T> double foo(T dings);
Was die Methode tut, sei erstmal egal, wir sehen uns zunächst nur die Signatur an:
foo
sei eine Methode, die einen Parameter des generischen Typs T
übernimmt und als Ergebnis einen
double-Wert liefert. Der parametrisierte Typ T
wird dazu in spitzen Klammern vor dem Rückgabe-Wert angegeben.
Er kann dann als Typ in der Parameterliste und als Rückgabe-Typ verwendet werden. Im obigen Beispiel brauchen wir ihn
nur für den Parameter dings
. Da im Beispiel keine Einschränkungen gemacht werden, kann T
beim Aufruf
der Methode durch beliebige Klasse oder ein beliebiges Interface ersetzt werden. Der Aufruf
foo(Integer.valueOf(1));
ist ebenso zulässig wie der Aufruf
foo("1");
Um mit dem Parameter etwas Sinnvolles anstellen zu können, müssen wir ihn etwas einschränken. Zunächst sollen
nur Klassen erlaubt werden, die das Interface Behaelter
implementieren:
public <T extends Behaelter> double wasDrinIst(T dings) { return dings.volumen(); }
Der Parameter dings
muß damit ein Objekt einer Klasse sein, die das Interface Behaelter
implementiert und
daher können wir darauf die Methode volumen()
aufrufen. Tatsächlich ist die generische Methoden-Deklaration
äquivalent zu dieser klassischen Variante:
public double wasDrinIst(Behaelter dings) { return dings.volumen(); }
Und jetzt kommt der Trick: In der klassischen Variante ohne Generics kann der Parameter dings
nur einen einzigen
Typ haben, im Beispiel ist das das Interface Behaelter
. Im Gegensatz dazu kann der generische Typ T
mehrere
Typen haben, die durch den &
-Operator verknüpft werden. Die endgültige Methode zur Berechnung des Gewichts
sieht dann so aus:
public <T extends Behaelter & Inhalt> double gewicht(T dings) { return dings.volumen() * dings.dichte(); }
T
ist nun ein Typ, der die Interfaces Behaelter
und Inhalt
implementiert.
Damit können wir die Methode volumen()
aus dem Interface Behaelter
und die Methode
dichte()
aus dem Interface Inhalt
auf das Objekt dings
anwenden. Tatsächlich
kann man beliebig viele Typen angeben, die mit den &
-Operator verknüpft werden. Darunter darf
auch eine Klasse sein, diese muß dann aber an erster Stelle stehen. Korrekt – wenn auch redundant – wäre
folgende Definition:
public <T extends Object & Behaelter & Inhalt> double gewicht(T dings) { return dings.volumen() * dings.dichte(); }
Die Anwendung der Interface-Kombination ist nicht auf Methoden beschränkt. Man kann sie auch zur parametrisierten Definition generischer Klassen verwenden:
public class Ladung <T extends Behaelter & Inhalt> { List<T> fracht = new ArrayList<>(); }
Den Code zu den aufgeführten Beispielen giebt's auf Git-Hub.