Falconbyte unterstüzen
Betrieb und Pflege von Falconbyte brauchen viel Zeit und Geld. Um dir auch weiterhin hochwertigen Content anbieten zu können, kannst du uns sehr gerne mit einem kleinen "Trinkgeld" unterstützen.
Schnelles Code-Beispiel:
ArrayList<Starship> liste = new ArrayList<>();
liste.add(new Starship());
- Nicht-generische Klassen
- Generische Klassen
- Eigene generische Klassen
- Übungen
Inhaltsverzeichnis
Nicht-generische Klassen
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.
Generische Klassen
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!
Vorteile generischer Klassen
Fassen wir die Vorteile der generischen Klassen nochmal zusammen:
- Erhöhte Typsicherheit
- Weniger explizites Casten
- Klarer und effizienter Code
- Homogene Sammlungsstrukturen
Eigene generische Klassen
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:
- Es können bei der Instanziierung alle möglichen Typen für T eingesetzt werden.
- Obwohl Paket<T> keine andere Klasse kennt, kann sie mit allen eingesetzten Klassen umgehen.
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.
Mehrere Typ-Parameter
Lust auf mehre 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.
Namenskonventionen für Typ-Parameter
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:
- T für den generischen Datentyp (Standard)
- S,U,V,.. für multiple Typen
- E für ein Element einer Sammlung des Java Collections Frameworks
- K für einen Map-Key
- V für einen Map-Value
- N für eine Zahl
Wir gehen in den nächsten Kapiteln näher auf die einzelnen generischen Typen Map-Key und Map-Value ein.
Übungen
einfach
Deklariere und initialisiere eine ArrayList als generische Klasse für Strings.
mittel
Was ist das Ergebnis des folgenden Codes?
ArrayList list = new ArrayList();
list.add("Space");
list.add("Lab");
list.add(2030);
for(String sV : list){
System.out.println(sV);
}
A. SpaceLab
B. SpaceLab2030
C. SpaceLab und eine Laufzeit-Exception
D. Code compiliert nicht
schwer
Der folgende Code enthält einen Compilier-Fehler. In welcher Zeile befindet er sich und wie lässt er sich beheben?
package p1;
import java.util.ArrayList;
public class Verkehrsteilnehmer<T,S> {
private T fahrer;
private S fahrzeug;
public Verkehrsteilnehmer(T fahrer, S fahrzeug){
this.fahrer = fahrer;
this.fahrzeug = fahrzeug;
}
public void fahrzeugAendern(S fahrzeugNeu){
fahrzeug = fahrzeugNeu;
}
public T getFahrer() {
return fahrer;
}
public S getFahrzeug() {
return fahrzeug;
}
public static void main(String[] args) {
ArrayList<Verkehrsteilnehmer<Person, String>> teilnehmer = new ArrayList<>();
Verkehrsteilnehmer<Person, Auto> t1 = new Verkehrsteilnehmer<>(new Person("Thomas M."), new Auto("BMW"));
teilnehmer.add(t1);
}
}