Alle Tabs der Lerneinheit (Erklärung · Interaktiv verstehen · Praxis-Übung · Klausur-Quiz) als durchgehender Text. Ideal zum Wiederholen vor der Klausur, und für Suchmaschinen wie Google, Bing und KI-Suche (ChatGPT, Perplexity).
Erklärung
Generics, Type-Parameter
List<String>, Map<Integer, Student>, Optional<T>, diese spitzen Klammern sind Generics. Sie sagen dem Compiler: "Diese Liste enthält NUR Strings, kein Integer, kein Object." Damit fängt Java Tippfehler beim Kompilieren statt erst zur Laufzeit mit einer ClassCastException. Du lernst hier, wie du eigene generische Klassen und Methoden schreibst, was Bounded Type-Parameter (T extends Comparable<T>) bedeuten, wie du Wildcards (<?>, <? extends X>, <? super X>) liest und mit der PECS-Regel anwendest, und warum Type-Erasure dafür sorgt, dass <T> zur Laufzeit verschwindet. In der Klausur löst dir das genau diese Fragetypen: Raw Types erkennen, Wildcards lesen, PECS anwenden, Type-Erasure erklären und Compile-Fehler vorhersagen.
Klausur-Pflicht in 10/13 WInf-Prog-1-Klausuren, Type-Parameter, Wildcards, Type-Erasure sind Standard-Stoff.
Die Idee in einem Satz
Generics: Du parametrisierst eine Klasse oder Methode mit einem Typ, statt einen festen Typ hart zu kodieren.
Box<T>ist eine Box für irgendeinen Typ T, der beim Anlegen festgelegt wird.
Warum überhaupt Generics?, der historische Schmerz
Vor Java 5 gab es keine Generics. Listen sahen so aus:
List liste = new ArrayList();
liste.add("Hallo");
liste.add(42); // kein Fehler! Objekt = alles geht
String s = (String) liste.get(1); // CRASH zur Laufzeit
Die Liste konnte alles speichern, weil intern Object. Beim Rausholen musstest du casten, und wenn du falsch lagst, ClassCastException zur Laufzeit. Bug findet dich, nicht du den Bug.
Mit Generics:
List<String> liste = new ArrayList<>();
liste.add("Hallo");
liste.add(42); // ← Compile-Fehler! int passt nicht in List<String>
String s = liste.get(0); // kein Cast nötig
Type-Safety zur Compile-Zeit. Der ganze Vorteil von Generics in einem Satz.
Eigene generische Klasse: Box<T>
public class Box<T> {
private T inhalt;
public void setInhalt(T inhalt) {
this.inhalt = inhalt;
}
public T getInhalt() {
return inhalt;
}
}
Nutzen:
Box<String> textBox = new Box<>();
textBox.setInhalt("Hallo");
String s = textBox.getInhalt(); // kein Cast
Box<Integer> zahlBox = new Box<>();
zahlBox.setInhalt(42);
// zahlBox.setInhalt("Text"); // Compile-Fehler!
Das T ist nur ein Platzhalter. Beim Anlegen (new Box<String>()) behandelt der Compiler Box<String> typsicher als Box für Strings: Argumente und Rückgaben werden gegen String geprüft. Im Bytecode wird T später durch Type-Erasure verarbeitet (dazu unten mehr), zur Laufzeit existiert der konkrete Typparameter nicht mehr. Konvention: ein Buchstabe (T für Type, E für Element, K/V für Key/Value).
Generische Methode
Auch einzelne Methoden können generisch sein, unabhängig von der Klasse:
public class Util {
public static <T> T erstes(List<T> liste) {
return liste.get(0);
}
}
String s = Util.<String>erstes(myStringList); // explizit
String s2 = Util.erstes(myStringList); // Java schließt T = String
Das <T> vor dem Rückgabetyp deklariert den Type-Parameter dieser Methode. Achte genau auf die Position, dasselbe T taucht in einer Signatur in zwei völlig verschiedenen Rollen auf:
DEKLARATION RÜCKGABETYP
│ │
▼ ▼
public static <T> T erstes(List<T> liste)
─── ─ ─
│ │ │
"ab hier ist T "diese Methode "Element-Typ der
ein Typ-Param" liefert ein T" Eingabe-Liste"
Das erste <T> (in spitzen Klammern) ist die Deklaration, sie führt den Namen T ein. Jedes weitere T (Rückgabetyp, Parameter) ist eine Verwendung dieses Namens. Ohne das vorangestellte <T> wüsste der Compiler nicht, dass T ein Type-Parameter und kein realer Klassenname ist.
Bounded Type-Parameter: T extends Comparable<T>
Manchmal brauchst du eine Bedingung an T, z.B. "T muss vergleichbar sein":
public static <T extends Comparable<T>> T max(List<T> liste) {
T grösstes = liste.get(0);
for (T element : liste) {
if (element.compareTo(grösstes) > 0) {
grösstes = element;
}
}
return grösstes;
}
Jetzt darf T nur etwas sein, das Comparable ist, und du darfst compareTo aufrufen.
extends heißt hier sowohl "erbt von" als auch "implementiert" (Interfaces). Java spart sich ein zweites Wort für Interfaces in Generics-Bounds.
Profi-Hinweis: Robuster ist in echtem Java-Code
<T extends Comparable<? super T>>, weil dann auch Typen erlaubt sind, derencompareTovon einem Obertyp geerbt ist. Für Prog-1 reicht aber<T extends Comparable<T>>.
Wildcards: <?>, <? extends X>, <? super X>
Wildcards braucht man bei Methoden-Parametern, wenn der konkrete Typ egal sein darf:
| Wildcard | Bedeutung | Beispiel |
|---|---|---|
List<?> | Liste von irgendwas (read-only) | universelle Ausgabe-Funktion |
List<? extends Number> | Liste von Number oder Subklassen (Integer, Double) | nur lesen ("Producer") |
List<? super Integer> | Liste von Integer oder Oberklassen (Number, Object) | nur schreiben ("Consumer") |
PECS-Regel: Producer Extends, Consumer Super. Wenn die Liste dir Werte liefert, nutze extends. Wenn du Werte reinschreibst, nutze super.
liefert dir Werte (Producer) du schreibst Werte rein (Consumer)
──── get() ───► ◄─── add() ────
List<? extends Number> src dst List<? super Integer>
-> lesen erlaubt, add verboten -> add erlaubt, get nur als Object
Eselsbrücke: aus einem extends-Producer holst du nur heraus (kommt raus = du liest), in einen super-Consumer steckst du nur hinein (geht rein = du schreibst).
Type-Erasure: der Compiler-Trick
Im fertigen Bytecode verschwindet das <T>! Java ersetzt T durch Object (oder bei Bounds durch die obere Schranke) und fügt Casts ein, wo nötig. List<String> und List<Integer> sind zur Laufzeit dieselbe Klasse.
Merksatz, was bleibt vs. was verschwindet: Der Klassen-Typ existiert zur Laufzeit weiter,
String.classist real und abfragbar. Der generische Typparameter verschwindet:List<String>undList<Integer>haben dieselbe Runtime-Klasse (ArrayList). Es verschwinden also nicht alle Typinformationen, nur die<...>-Parameter.
So sieht derselbe Code vor und nach der Erasure aus:
VORHER (dein Quellcode, Compile-Zeit) NACHHER (Bytecode, Laufzeit-Sicht)
───────────────────────────────────── ──────────────────────────────────
class Box<T> { class Box {
T inhalt; Object inhalt; // T -> Object
T getInhalt() { return inhalt; } Object getInhalt() { return inhalt; }
} }
Box<String> b = new Box<>(); Box b = new Box();
String s = b.getInhalt(); String s = (String) b.getInhalt(); // Cast eingefügt
Der Compiler garantiert die Typsicherheit also VOR der Erasure und packt danach die nötigen Casts selbst rein. Konsequenzen:
List<String> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
l1.getClass() == l2.getClass(); // true! Beide geben ArrayList.class zurück (true, weil == hier Identity prüft, also dasselbe Class-Objekt im Speicher, nicht equals())
// und das hier ist KEIN Compile-Fehler:
if (obj instanceof List) { ... } // erlaubt
// aber das hier IST ein Compile-Fehler:
if (obj instanceof List<String>) { ... } // nicht erlaubt
Du kannst auch keine Generic-Arrays anlegen:
T[] array = new T[10]; // Compile-Fehler
List<String>[] arr = new List<String>[5]; // Compile-Fehler (parameterisierter Typ)
Was hingegen erlaubt ist, zum Kontrast: ein Array mit unbounded Wildcard, weil List<?> ein reifizierbarer Typ ist (keine konkrete Typ-Information, die durch Erasure verloren ginge):
List<?>[] arr = new List<?>[5]; // erlaubt
Brauchst du wirklich ein generisches Array, gibt es zwei Wege. Der saubere: einfach List<T> statt Array verwenden, dann hast du das Problem nie. Der unschöne Workaround mit Cast:
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10]; // kompiliert mit Warning, Risiko: Heap-Pollution
Hier unterdrückst du die unchecked-Warnung bewusst. Gefährlich ist das, weil zur Laufzeit ein Object[] entsteht: Versuchst du es als typisiertes Array nach außen zu geben, drohen versteckte ClassCastException. Im Zweifel List<T> nehmen.
Klausur-Faustregeln
1. List<String> heißt: NUR Strings. Compile-Time-Garantie, kein Cast beim Rausholen.
2. <T> ist ein Platzhalter. Konkretisiert beim Anlegen: new Box<Integer>().
3. Diamond-Operator <> (Java 7+): rechts kannst du den Typ weglassen, Compiler schließt ihn. Map<String, List<Integer>> m = new HashMap<>();
4. Type-Erasure. Zur Laufzeit existiert das <T> nicht mehr. getClass() zeigt nur ArrayList, nicht ArrayList<String>.
5. PECS-Regel. Producer extends, Consumer super. Wenn unsicher: ohne Wildcard erstmal probieren, beim Methoden-Übergreifen Wildcards einsetzen.
Häufige Stolpersteine
1. new T() versuchen. Geht nicht, weil T zur Laufzeit nicht existiert. Workaround: T als Class<T> Parameter mitgeben oder Supplier<T> verwenden.
2. List<Integer> an List<Number> Parameter übergeben. Geht nicht! Generics sind invariant: List<Integer> ist NICHT subtyp von List<Number>, obwohl Integer extends Number. Wenn du das brauchst: Wildcard List<? extends Number>.
3. Primitive Typen in Generics. List<int> ist Compile-Fehler. Generics brauchen Object-Typen. Nutze die Wrapper: List<Integer>, List<Double>. Autoboxing macht den Rest.
4. Diamond <> vor Java 7 vergessen. In modernem Code IMMER mit Diamond schreiben, sonst Raw-Type-Warnung.
5. Raw-Types verwenden. List l = new ArrayList(); (ohne <...>) ist nur aus Legacy-Kompatibilität noch erlaubt (Java garantiert Rückwärts-Kompatibilität mit Vor-Java-5-Code), aber veraltet und dringend zu vermeiden: Der Compiler erzeugt unchecked- bzw. raw-type-Warnings und du verlierst die komplette Typsicherheit. Raw Types sind formal nicht als @Deprecated markiert, du solltest sie aber wie veralteten Code behandeln und in neuem Code meiden. In neuem Code immer den konkreten Typ oder eine Wildcard angeben.
Klausur-Beispielaufgabe
Beispiel (typischer Klausur-Stil, zur Übung konstruiert).
Gegeben ist eine Methode, die alle Elemente von src nach dst kopiert:
static <T> void copy(List<___> src, List<___> dst) {
for (T element : src) {
dst.add(element);
}
}
(a) Welche Wildcards gehören in die beiden Lücken, damit man z. B. aus einer List<Integer> in eine List<Number> kopieren kann? (b) Begründe mit der PECS-Regel. (c) Warum wäre List<T> für beide Parameter zu streng?
Lösung:
- (a)
static <T> void copy(List<? extends T> src, List<? super T> dst). Damit akzeptiertsrceine Liste vonToder einem Subtyp,dsteine Liste vonToder einem Obertyp. - (b)
srcist der Producer: du liest nur Werte heraus (elementkommt aussrc), alsoextends(Producer Extends).dstist der Consumer: du schreibst nur Werte hinein (dst.add(...)), alsosuper(Consumer Super). Genau das ist PECS. - (c) Mit
List<T>für beide müsstensrcunddstexakt denselben Typ-Parameter haben. Wegen der Invarianz von Generics wäre danncopy(List<Integer>, List<Number>)ein Compile-Fehler, obwohlIntegerein Subtyp vonNumberist. Die Wildcards lockern genau diese Einschränkung. (Diese Signatur entspricht der echtenCollections.copy-API, vereinfacht; die echte API prüft zusätzlich zur Laufzeit, dassdst.size() >= src.size(), sonst eineIndexOutOfBoundsException.)
Interaktiv verstehen
Generics-Type-Toggle
Probier verschiedene Typ-Parameter aus (z.B. String, Integer, Object). Beobachte: identischer Code, andere Typen, Compile-Fehler erscheinen / verschwinden.
Wähle den Typ T für Box<T> aus und sieh, welche Operationen erlaubt sind und welche der Compiler ablehnt. Probiere etwa, was passiert, wenn du textBox.setInhalt(42) bei einer Box<String> aufrufst, oder zahlBox.setInhalt("Text") bei einer Box<Integer>.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Klausur-Tipp: Bei Klausurfragen "Welche Zeile wirft einen Compile-Fehler?", schau auf den deklarierten Generic-Typ und prüfe, ob der zugewiesene Wert/Typ passt. Generics-Fehler werden vom Compiler erkannt, NICHT erst zur Laufzeit.
Code-Stepper: Schritt für Schritt
Drei Demos zum Durchklicken. Klick Step oder Auto und beobachte, wie sich Typ-Check, Variablen und Ausgabe pro Schritt ändern.
1. Bounded Type-Parameter: max() über Comparable
Die generische Methode max läuft durch eine Liste und nutzt compareTo, das nur erlaubt ist, weil T extends Comparable<T> gefordert wird.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
2. PECS in Aktion: kopiere(src, dst)
Klassische Wildcard-Signatur. src ist Producer (extends), dst ist Consumer (super). Beobachte, wie ein Integer aus der Producer-Liste in die Consumer-Liste wandert.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
3. Type-Erasure: getClass() ist identisch
Zwei Listen mit verschiedenen Typ-Parametern, aber zur Laufzeit dieselbe Klasse. Der Stepper zeigt, warum der ==-Vergleich true ergibt.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Praxis-Übung
Generics, Praxis-Übung
6 Aufgaben zu Type-Parameter, Wildcards, Type-Erasure.
Klausurfragen mit Lösungen (6)
- F1.Was ist der Hauptvorteil von Generics?
Antwort: Type-Safety zur Compile-Zeit
Erklärung: Generics geben dir Type-Safety BEIM KOMPILIEREN. Type-Mismatches werden vom Compiler erkannt, statt erst zur Laufzeit als ClassCastException zu crashen. Keine Performance-Vorteile (eher gleich), Speicher unverändert.
- F2.Welche Zeile erzeugt einen Compile-Fehler?
Antwort: Zeile 3: liste.add(42)
Erklärung: Zeile 3 ist ein Compile-Fehler: 42 (Integer) passt nicht in List<String>. Das ist exakt der Schutz, den Generics geben. Zeile 4 wäre ohne Generics ein Cast (String), mit Generics nicht nötig.
- F3.Zur Laufzeit unterscheidet die JVM zwischen List<String> und List<Integer>.
Antwort: Falsch
Erklärung: FALSCH. Type-Erasure: zur Laufzeit sind beide einfach ArrayList. Die Typ-Information ist nur zur Compile-Zeit da. Deshalb: liste.getClass() == liste2.getClass() ist TRUE, auch bei unterschiedlichen Generic-Typen.
Typ: Wahr/Falsch
- F4.Ist List<Integer> ein Subtyp von List<Number>?
Antwort: Nein, Generics sind invariant
Erklärung: Generics sind INVARIANT, auch wenn Integer Subtyp von Number ist, ist List<Integer> KEIN Subtyp von List<Number>. Wäre fatal, weil du sonst einen Double in eine List<Integer> reinschmuggeln könntest. Wenn du das brauchst: Wildcard List<? extends Number>.
- F5.Welche Aussagen über Wildcards sind RICHTIG?
Richtige Antworten: List<?> erlaubt jeden Typ als Inhalt; List<? extends Number> erlaubt nur Lesen, kein Schreiben (außer null); List<? super Integer> erlaubt das Hinzufügen von Integer-Werten; PECS: Producer extends, Consumer super
Erklärung: Richtig: List<?> Beliebig, extends → nur lesen, super → schreiben ok, PECS-Regel. Falsch: Wildcards sind Compile-Time, keine Perf-Auswirkung. List<?> != List<Object>: List<String> ist Subtyp von List<?> aber NICHT von List<Object>.
Typ: Multi-Select
- F6.Ordne Generic-Konstrukt zur Bedeutung zu:
Zuordnungen:
- <T> → Type-Parameter (Platzhalter, wird konkretisiert)
- <T extends Comparable<T>> → Bounded: T muss Comparable sein
- <?> → Wildcard: irgendein Typ
- <? super Integer> → Integer oder Oberklasse von Integer
Erklärung: Standard-Generic-Vokabular. Sitzen lernen, kommt in 1-2 Klausurfragen vor.
Typ: Zuordnung
List<String> liste = new ArrayList<>();
liste.add("Hallo");
liste.add(42);
String s = liste.get(0);Klausur-Quiz
Klausurfragen mit Lösungen (6)
- F1.Welcher Buchstabe ist Konvention für 'Type-Parameter einer Liste'?
Antwort: E
Erklärung: E = Element (List<E>, Set<E>). T = generischer Type, K = Key, V = Value (Map<K,V>). Nur Konvention, technisch wäre jeder Buchstabe erlaubt.
- F2.Was bedeutet der Diamond-Operator <> in: List<String> liste = new ArrayList<>();?
Antwort: Diamond: Compiler schließt den Typ aus dem linken Kontext
Erklärung: Seit Java 7 darf der Typ auf der rechten Seite weggelassen werden, wenn er aus dem linken Kontext (Variable-Deklaration) ableitbar ist. Compiler ergänzt automatisch <String>.
- F3.Man kann ein generisches Array anlegen: T[] arr = new T[10];
Antwort: Falsch
Erklärung: FALSCH. Wegen Type-Erasure ist das Compile-Fehler. Java weiß zur Laufzeit nicht, was T war, und kann das Array nicht typsicher allokieren. Workaround: List<T> verwenden oder (T[]) Array.newInstance(...).
Typ: Wahr/Falsch
- F4.Welche Methoden-Signatur erlaubt 'kopiere alles von src nach dst', wenn dst die Werte vom Typ T oder einer Oberklasse aufnehmen darf?
Antwort: <T> void kopiere(List<? extends T> src, List<? super T> dst)
Erklärung: PECS in Aktion! src ist PRODUCER → extends (src.get(i) liefert T oder Subtyp). dst ist CONSUMER → super (dst.add() akzeptiert T oder Subtyp). Klassische Java-API-Signatur (siehe Collections.copy).
- F5.Generics sind {{1}}: List<Integer> ist KEIN Subtyp von List<Number>. Zur Laufzeit existiert das <T> nicht mehr, das nennt man {{2}}. Die PECS-Regel lautet: Producer {{3}}, Consumer {{4}}.
Lösungen pro Lücke:
- {{1}}: invariant
- {{2}}: Type-Erasure / Type Erasure / Typ-Erasure
- {{3}}: extends
- {{4}}: super
Erklärung: Generics-Kern-Vokabular. Invariant, Type-Erasure, PECS, wer diese 3 Begriffe sicher erklären kann, hat Generics verstanden.
Typ: Lückentext
- F6.Was ist der korrekte Compile-Schritt für List<String> l = new ArrayList<>(); l.add("x"); String s = l.get(0);?
Richtige Reihenfolge:
- Compiler prüft alle add(...) gegen Generic-Typ
- Type-Erasure: T → Object im Bytecode
- Compiler fügt automatisch (String)-Cast beim get(0) ein
- Zur Laufzeit: ArrayList speichert Object-Referenzen
Erklärung: Compile-Zeit: Typ-Check. Bytecode-Generierung: Erasure + Cast-Insertion. Laufzeit: nur noch Object. So liefert Java Type-Safety ohne JVM-Generics.
Typ: Reihenfolge