Overview
Generische Klassen (engl. Generics) gehöhren seit Java Version 5 zu den fest etablierten Sprachmitteln. Sie kommen insbesondere (aber nicht nur) in den Datenstrukturen des Collections Frameworks zum Einsatz und helfen, diese effizient zu verwalten.
Generische Klassen (engl. Generics) gehöhren seit Java Version 5 zu den fest etablierten Sprachmitteln. Sie kommen insbesondere (aber nicht nur) in den Datenstrukturen des Collections Frameworks zum Einsatz und helfen, diese effizient zu verwalten.
Was das bedeutet, können wir am Besten erkennen, wenn wir uns die Verwendung einer nicht-generischen Liste ansehen, also der altmodischen Art und Weise, wie wir Objektsammlungen vor Java 5 verwaltet haben:
ArrayList liste = new ArrayList();
liste.add(new Starship());
Die Klasse ArrayList kennen wir in generischer Form bereits. Was wir hier sehen, ist die alte nicht-generische Verwendung.
Nicht-generische Sammlungsklassen verwalten grundsätzlich Objekte vom Typ Object, das bedeutet im Klartext, dass jeder beliebige Objekttyp in die Sammlung aufgenommen werden kann (denn alle Objekte sind von der Superklasse Object abgeleitet).
Was sich zunächst ganz praktisch anhört, ist es nicht. Wie wollen wir so eine homogene Objektsammlung erzeugen? Erschwerend kommt hinzu, dass beim Auslesen stets Objekte vom Typ Object geliefert werden. Das macht ein explizites Casten erforderlich:
Starship sn = (Starship) liste.get(0);
Dass wir jedes Mal einen Cast durchführen müssen ist nicht nur umständlich, sondern auch riskant. Stellt sich nämlich zur Laufzeit heraus, dass das zurückgelieferte Objekt gar nicht vom Typ Starship ist, bekommen wir eine ClassCastException entgegengeschmissen.
Wir sollten nicht-generischen Sammlungsklassen heute deshalb nicht mehr einsetzen.
Bei generischen Klassen geht es im Wesentlichen darum, dem Compiler mehr Typ-Informationen zu geben und ClassCastException-Fehler zu reduzieren.
Setzen wir die ArrayList also als generischen Typ um:
ArrayList<Starship> liste = new ArrayList<>();
Wir erkennen generische Klassen am formalen Typ-Parameter in den spitzen Klammern. Der formale Typ-Parameter legt bei der ArrayList fest, welche Typen in die Sammlung aufgenommen werden dürfen. Die generische ArrayList im Beispiel ist mit dem Typ Starship parametisiert.
Es können also nur Objekte vom Typ Starship (und Unterklassen davon) aufgenommen werden, was eine homogene Sammlung ermöglicht. Wird dennoch versucht, einen nicht kompatiblen Typ aufzunehmen, wird das bereits zur Compilierzeit unterbunden:
ArrayList<Starship> liste = new ArrayList<>();
liste.add(new Starship());
liste.add(new Fahrrad()); // Fehler!
liste.add("Ich will auch"); // Fehler!
Der Compiler-Fehler ist gut - wir bekommen nämlich bereits vor dem Start des Programms mitgeteilt, dass etwas nicht stimmt und können den Fehler gleich beheben, ohne ihn später unter Umständen mühsam suchen zu müssen.
Außerdem brauchen wir beim Herausholen der Objekte nicht mehr zu casten:
Starship sn1 = liste.get(0); // passt!
Was für ein Fortschritt!
Fassen wir die Vorteile der generischen Klassen nochmal zusammen:
Du kannst auch eigene generische Klassen schreiben. Der formale Typ-Parameter wird dann in eckigen Klammern direkt hinter dem Namen der Klasse ergänzt:
public class Paket<T>{
private T inhalt;
public T inhaltAuspacken(){
return inhalt;
}
public void inhaltEinpacken(T inhalt){
this.inhalt = inhalt;
}
}
Der generische Typ T kommt innerhalb der Klasse mehrfach vor. Das symbolische T steht dabei stellvertretend für den tatsächlichen Typ, der bei der Objekterzeugung in den spitzen Klammern eingesetzt wird:
Paket<Notebook> p1 = new Paket<>();
p1.inhaltEinpacken(new Notebook());
Der Typ-Parameter T ist nur symbolisch und steht für keine spezielle Klasse. Das hat folgenden Effekt:
Wie universell unser Typ-Parameter ist, sehen wir, wenn wir für T einen ganz anderen Typ einsetzen, der mit der Klasse Notebook garnichts zu tun hat:
Paket<Starship> p2 = new Paket<>();
p2.inhaltEinpacken(new Starship());
Ein Starship zu verpacken ergibt in der Realität vielleicht nicht viel Sinn, aber wir sehen dadurch, dass wir schlichtweg alle Typen für T einsetzen können. Wir sind an keine Vererbungshierarchie gebunden. Als es noch keine Generics gab, hätten wir in der Paket-Klasse mit Object arbeiten müssen, was wiederum umständliches Casting und das Risiko von ClassCastExceptions bedeutet hätte.
Lust auf mehrere Typ-Parameter? Kein Problem, wir können problemlos eine multiple generische Klasse definieren:
public class Paket <T, S>{
private T inhalt;
private S empfaenger;
public void setAll(T inhalt, S empfaenger){
this.inhalt = inhalt;
this.empfaenger = empfaenger;
}
}
T steht für den Inhalt des Pakets und S repräsentiert den Empfänger. Um die multiple generische Klasse zu verwenden, schreiben wir folgenden Code:
Paket<Notebook, String> paket = new Paket<>();
paket.setAll(new Notebook(), "Elon");
Die Empfänger-Klasse ist hier String, hätte aber auch jeder andere Objekttyp sein können.
Natürlich können wir den Typ-Parameter bennen, wie es uns gefällt, wir sollten das aber unbedingt nicht tun. Würden wir etwa statt T eine existierende Klasse (z.B. Notebook) wählen, würden das dem universellen Charakter der generischen Klassen zuwiederlaufen.
Der Name der Typ-Paramter ist symbolisch und wird konventionell mit einem einzelnen Zeichen beschrieben:
Wir gehen in den nächsten Kapiteln näher auf die einzelnen generischen Typen Map-Key und Map-Value ein.
Java Basics
[Java einrichten] [Variablen] [Primitive Datentypen] [Operatoren] [if else] [switch-case] [Arrays] [Schleifen]
Objektorientierung
[Einstieg] [Variablen ] [Konstruktor] [Methoden] [Rekursion] [Statische Member] [Initializer] [Pass-by-value] [Objektsammlungen] [Objektinteraktion] [Objekte löschen]
Klassenbibliothek
[Allgemeines] [String ] [Math] [Wrapper] [Scanner] [java.util.Arrays] [Date-Time-API]
Vererbung
[Einstieg Vererbung] [Konstruktoren bei Vererbung ] [Der protected Zugriffsmodifikator] [Abstrakte Klassen und Methoden] [Polymorphie in Java] [Typumwandlung] [Die Klasse Object] [Die toString()-Methode] [Objekte vergleichen] [Was ist ein Interface?]