Hintergrund
Generische Klassen sind ein wichtiger Baustein beim Entwurf wiederverwendbarer Objektmodelle. Man findet diese unter anderem in den Bibliotheken wie der Java API oder dem .NET-Framework. (Hier unter anderem bei den diversen Collections wie Listen und Hashsets).
Eine generische Klasse ist eine Vorlage für eine Menge von gleichartigen Klassen. Gleichartig insofern, als dass der Inhalt der Klassen bzgl. Eigenschaften und Verhalten identisch ist; dieser sich allerdings hinsichtlich der verwendeten Typen unterscheiden kann.
Generische Klassen verwenden für die Typisierung das Mittel der Vererbung. In diesem Artikel möchte ich anhand eines Beispiels aus der Tierwelt einen tieferen Einblick in die Umsetzung und Verwendung vermitteln. Für die Implementierung verwende ich hier Java.
An dieser Stelle sei auf anderen anderen Artikel verwiesen: https://www.train-your-brain.online/gemueseanbau-mit-generischen-klassen/
Die Anwendung Teil 1 – Aufbau einer generischen Klasse
Wir gehen gedanklich in den Zoo. Dort gibt es für die verschiedenen Tiere Gehege. Jedes Gehege hat dabei grundlegende Eigenschaften und Funktionalitäten:

So hat jedes Gehege eine Größe und einen Ort, an dem es sich befindet. Des Weiteren kann man jedes Gehege öffnen und schließen.
Weitere Ausstattungsmerkmale sind abhängig von den beherbergten Tieren. Dabei kann es Gehege geben, die nur eine Tierart beherbergen; aber auch Gehege, in denen viele verschiedene Tierarten wohnen.
Somit können wir unabhängig von der Klasse Gehege die Tiere definieren. Für dieses Beispiel definieren wir eine Klasse Kleintiere. Ein Kleintier ist allerdings sehr allgemein; wir können hier noch zwischen Vögeln (wie Papagei und Geier) sowie dem Graumull unterscheiden.

Ein Gehege beherbergt – wie erwähnt – verschiedene Tiere. Es handelt sich somit um eine Aggregationsbeziehung zwischen der Klasse Gehege und einer der Klassen der Vererbungsstrukturen des Kleintiers.

Wir sehen, dass die Klasse Gehege jeweils eine Liste mit Tieren enthält. Die Objekte auf der jeweiligen Liste haben aber einen unterschiedlichen Typ.
Anstatt nun für jede mögliche Aggregationsbeziehung eine eigene Klasse Gehege zu definieren, fassen wir alle Varianten unter einer generischen Klasse zusammen.

In der rechten oberen Ecke steht der Typparameter, welcher bei Verwendung einer solchen Klasser ergänzt werden muss. Hier wurde angegeben, dass das T (die allgemeine Typbezeichnung) in einer Vererbungsbeziehung zur Klasse Kleintier steht.
class Gehege<T extends Kleintier>
{
List<T> tiere;
}
Eine Verwendung von extends für den Typparameter ist nicht erforderlich. Mit dem Schlüsselwort extends schränken wir hier ein, dass nur Typen verwendet werden dürfen, die als Oberklasse die Klasse Kleintier haben.
Die Anwendung Teil 2 – Verwendung der generischen Klasse
Schauen wir uns als nächstes an, wie wir nun diese generischen Klasse in unseren Programmen verwenden können.
Ein Gehege für Kleintiere
Als Objekte sollen in das Gehege alle Tiere, die als Kleintiere bezeichnet werden können. Dazu gehören auch Vögel und der Graumull.
Gehege<Kleintier> gehege1 = new Gehege<>()
:
Aus dieser Deklaration heraus wird nun final folgende Klasse im Hintergrund erstellt:
class Gehege
{
List<Kleintier> tiere;
}
Der allgemeine Typbezeichner T wird also ersetzt mit dem in dem in den spitzen Klammern (diamond-operator) angegebenen Typ.
Ein Gehege für Vögel
Wollen wir nun ein Gehege, in dem nur Vögel – Papagei und Geier -leben dürfen, dann müssen wir den Typparameter etwas einschränken.
Gehege<Vogel> gehege2 = new Gehege<>();
Das gehege2 kann nun in seiner Liste aufnehmen Objekte der Klassen Papagei und Geier.
Ein Gehege für den Graumull
Natürlich können wir auch ein Gehege erstellen, in welchem nur ein bestimmtes Tier lebt; hier der Graumull.
Gehege<Graumull> gehege3 = new Gehege<>();
Zusammenfassend können wir erst einmal festhalten, dass der Typparameter einer generischen Klasse eine Restriktion vorgibt, Objekte welcher Typen in einer solchen Klasse verwendet werden können. Wir haben auch gesehen, dass eine solche Klasse erst durch ihre Verwendung in einem Programm final bestimmt wird (Übergabeparameter von Methoden, Rückgabetypen, Parameter bei Listen …). Um Objekte unterschiedlicher Typen mit einer solchen generischen Klasse verwenden zu können, orientieren wir uns am Aufbau einer Vererbungsstruktur. (beispiel: um Papagei und Geier in ein Gehege zu setzen, müssen beide eine gemeinsame Oberklasse haben, welche dann als Typparameter bei der generischen Klasse verwendet wird.)
Anwendung Teil 3 – Definition einer generischen Methode
Eine generische Methode ist entsprechend einer generischen Klasse eine Vorlage für eine Menge von Methoden.
So hat jede Methode einen Rückgabetyp und optional eine Menge von Übergabeparametern. Wir können eine Methode überladen, wenn die Anzahl und Typen der Übergabeparameter sich unterscheiden, jedoch der Inhalt identisch ist.
Erstellen wir nun exemplarisch eine Reihe von Methoden für den Besuch von unterschiedlichen Gehegen:
//Besuch eines Geheges mit Vögeln
public void visit(Gehege<Vogel> g){}
//Besuch eines Geheges mit Papageien
public void visit(Gehege<Papagei> g){}
//Besuch eines Geheges mit dem Graumull
public void visit(Gehege<Graumull> g){}
Kann man so machen … dann haben wir die Methode visit() überladen. Wenden wir diese Vorgehensweise an, so dürfen wir für jedes mögliche Gehege eine Methode visit definieren. Das kann sehr viel werden.
Aber zum Glück gibt es generische Methoden. Der Typ des Übergabeparameters in der Methode visit ändert sich. Daher verwenden wird hier einem Template-Parameter T.
public void visit(T g){}
Doch ein T alleine reicht nicht. Mit diesem T kann ich nun ein Objekt beliebigen Typs an die Methode übergeben. Wir müssen eine Einschränkung treffen: nur Gehege können übergeben werden:
public void visit(Gehege<T> g){}
Da das T weiterhin unbestimmt ist, können wir mit dieser Methode ein Gehege mit beliebigem Inhalt besuchen.
Möchten wir T weiter einschränken, so dass nur bestimmte Typen zulässig sind, verwenden wir einen den Wildcards extends oder super.

Anwendung Teil 4 – Verwendung generischer Methoden
Wir möchten eine Gehege besuchen, in welchem sich zumindest ein Graumull aufhält bzw auf Typdefinition aufhalten könnte. dazu gehören lt. Vererbungshierarchie die beiden folgenden Methoden visit mit den Gehegetypen:
public void visit(Gehege<Graumull> g){}
public void visit(Gehege<Kleintier> g){}
Fassen wir diese beiden Methoden zu einer generischen zusammen, so ergibt sich folgendes:
public void visitGraumull(Gehege<? super Graumull> wohin){}
Graumull definiert die untere Grenze in der Vererbungshierarchie.
Damit sind folgende Programmabläufe denkbar:
Gehege<Kleintier> g1 = new Gehege<>();
Gehege<Graumull> g2 = new Gehege<>();
visit(g1);
visit(g2);
Nehmen wir ein anderes Beispiel: Wir wollen ein Gehege mit Vögeln besuchen; ob Papagei oder Geier ist völlig egal.
Damit kommen die folgenden drei Methoden visit() in Betracht:
public void visit(Gehege<Vogel> g){}
public void visit(Gehege<Papagei> g){}
public void visit(Gehege<Geier> g){}
Im ersten Fall handelt es sich um ein gemischtes Gehege.
Nun fassen wir diese Methoden zu einer generischen zusammen. Laut Vererbungshierarchie ist die obere Grenze der Typ Vogel. Es werden alle Spezialisierungen dieser Klasse akzeptiert.
public void visit(Gehege<? extends Vogel> g){}
Damit sind nun die folgenden Programmabläufe denkbar:
Gehege<Vogel> g1 = new Gehege<>();
Gehege<Papagei> g2 = new Gehege<>();
Gehege<Geier> g3 = new Gehege<>();
visit(g1);
visit(g2);
visit(g3);