Generics und Klassen mit vielen Gesichtern

Aus MimiPedia

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.