Java Erweiterte Techniken – Tag 3 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting
Mit Einblicken von Nova Trent (Junior Dev) und Jamal Hassan (Backend Developer)
Schwierigkeit: 🟡 Mittel
Dauer: ~3 Stunden (Anfänger) | ~1,5 Stunden (mit Erfahrung)
Voraussetzungen: Tag 1-2 (Collections), Java OOP (Vererbung, Interfaces)
🗺️ Deine Position im Kurs
| Tag | Thema | Level | Status |
|---|---|---|---|
| 1 | Collections – Listen | 🟢 | ✅ Abgeschlossen |
| 2 | Collections – Sets & Maps | 🟢 | ✅ Abgeschlossen |
| → 3 | Generics – Typsicherheit | 🟡 | 👉 DU BIST HIER |
| 4 | Lambda-Ausdrücke | 🟡 | 🔜 Kommt als nächstes |
| 5 | Functional Interfaces | 🟡 | 🔒 |
| 6 | Stream-API | 🟡 | 🔒 |
| 7 | File I/O | 🟡 | 🔒 |
| 8 | Annotations & Multithreading | 🟡 | 🔒 |
| 9 | Multithreading – Synchronisation | 🔴 | 🔒 |
| 10 | Netzwerkprogrammierung | 🔴 | 🔒 |
Modul: Java Erweiterte Techniken
Dein Tempo:
- 🌱 Anfänger: ~3 Stunden – lies alles, mach alle Übungen
- 🌳 Profis: ~1,5 Stunden – spring zu PROFESSIONALS, hol dir die Challenges
📋 Bevor du startest
Du brauchst:
- ✅ Tag 1-2 abgeschlossen (Collections verstanden)
- ✅ Java OOP Basics (Vererbung, Interfaces)
- ✅ IDE deiner Wahl (IntelliJ, Eclipse, NetBeans)
- ✅ JDK 17+ installiert
🏃 Dein Tempo
Nimm dir die Zeit, die DU brauchst:
| Dein Level | Empfohlenes Tempo |
|---|---|
| 🌱 Neu dabei | 1 Tag pro Tag – lies alles, mach jede Übung |
| 🌿 Kannst schon programmieren | Überspring GRUNDLAGEN wenn sie klar sind |
| 🌳 Profi | Spring direkt zu PROFESSIONALS, hol dir die Challenges |
Kein Stress! Die Profis sind schneller – das ist okay. Du lernst in DEINEM Tempo.
⚡ Das Wichtigste in 30 Sekunden
Das Problem: Du schreibst eine Klasse, die „alles“ speichern kann. Ohne Generics musst du überall casten – und zur Laufzeit fliegen dir ClassCastExceptions um die Ohren.
Die Lösung: Generics – Typsicherheit zur Compile-Zeit. Der Compiler prüft die Typen, bevor dein Code überhaupt läuft.
Heute lernst du:
- ✅ Warum Generics erfunden wurden (und was vorher schief ging)
- ✅ Generische Klassen und Methoden selbst schreiben
- ✅ Bounded Type Parameters (
<T extends Number>) - ✅ Wildcards (
?,? extends,? super) – und WANN du welche brauchst - ✅ PECS verstehen und anwenden
- ✅ Type Erasure: warum
List<String>zur Laufzeit nurListist
Am Ende kannst du: Legacy-Code mit Raw Types erkennen, fixen und typsichere APIs designen.
👋 Hi! Elyndra hier.
Willkommen zu Tag 3! 🎉
In den letzten zwei Tagen hast du Collections gemeistert – und dabei ständig List<String> oder Map<String, Integer> geschrieben. Aber was bedeutet dieses <String> eigentlich? Und warum meckert der Compiler, wenn du es weglässt?
Egal ob du…
- 🌱 …gerade erst anfängst und
<T>noch kryptisch findest - 🌿 …schon Erfahrung hast, aber Wildcards vermeidest
- 🌳 …als Profi die Feinheiten von PECS verstehen willst
…dieser Tag hat was für dich!
Real talk: Generics gehören zu den Themen, die viele Entwickler „irgendwie“ verstehen – bis sie selbst eine generische API designen müssen. Dann wird’s plötzlich kompliziert.
Heute ändern wir das.
Los geht’s! 🚀
🟢 GRUNDLAGEN: Was sind Generics?

💡 Schon Profi? → Spring zu PROFESSIONALS | → Direkt zu den Challenges
Eine Geschichte aus der Java-Steinzeit
Vor Java 5 (also vor 2004!) sah Code so aus:
// Java 1.4 – ohne Generics
List namen = new ArrayList();
namen.add("Max");
namen.add("Anna");
namen.add(42); // Kompiliert! Aber... warum erlaubt das der Compiler?!
// Später im Code...
for (Object obj : namen) {
String name = (String) obj; // BOOM! ClassCastException bei 42
System.out.println(name);
}
Was macht dieser Code?
Er erstellt eine Liste, fügt Strings hinzu – und dann eine Zahl. Der Compiler sagt: „Alles klar!“ Erst zur Laufzeit, wenn du versuchst 42 zu einem String zu casten, explodiert alles.
Warum ist das ein Problem?
Das Problem ist nicht der Cast an sich. Das Problem ist, dass der Fehler erst zur Laufzeit auftritt – vielleicht Wochen nach dem Deployment, vielleicht um 3 Uhr nachts, vielleicht beim wichtigsten Kunden.
Die Lösung seit Java 5:
// Java 5+ – mit Generics
List<String> namen = new ArrayList<>();
namen.add("Max");
namen.add("Anna");
namen.add(42); // COMPILE ERROR! int ist kein String
for (String name : namen) { // Kein Cast nötig!
System.out.println(name);
}
Was ist hier anders?
List<String>sagt dem Compiler: „Diese Liste enthält NUR Strings.“- Der Compiler prüft jeden
add()-Aufruf. namen.add(42)wird sofort als Fehler markiert – nicht erst zur Laufzeit.- Beim Iterieren brauchst du keinen Cast mehr.
Das Prinzip dahinter:
Generics verschieben Typprüfungen von der Laufzeit zur Compile-Zeit. Der Compiler wird dein Sicherheitsnetz. Fehler werden gefunden, bevor der Code überhaupt läuft.
Wichtig zu verstehen:
Diese Sicherheit kostet nichts zur Laufzeit. Generics sind reine Compile-Zeit-Magie – dazu später mehr bei Type Erasure.
Generische Klassen selbst schreiben
Du hast Generics bisher nur verwendet (List<String>). Jetzt schreiben wir selbst eine generische Klasse.
Das Szenario:
Du brauchst eine einfache „Box“, die einen beliebigen Wert speichern kann. Ohne Generics:
// SCHLECHT: Ohne Generics
public class ObjectBox {
private Object inhalt;
public void setInhalt(Object inhalt) {
this.inhalt = inhalt;
}
public Object getInhalt() {
return inhalt;
}
}
// Verwendung – hässlich und unsicher!
ObjectBox box = new ObjectBox();
box.setInhalt("Hallo");
String text = (String) box.getInhalt(); // Cast nötig!
Mit Generics:
public class Box<T> {
private T inhalt;
public void setInhalt(T inhalt) {
this.inhalt = inhalt;
}
public T getInhalt() {
return inhalt;
}
}
Was macht dieser Code?
Box<T> ist eine generische Klasse. Das T ist ein Type Parameter – ein Platzhalter für einen konkreten Typ, den der Nutzer der Klasse festlegt.
Wie funktioniert das im Detail?
Der Type Parameter <T>:
Das T in Box<T> ist wie eine Variable, aber für Typen statt für Werte. Wenn jemand Box<String> schreibt, ersetzt der Compiler gedanklich jedes T durch String.
Die Verwendung:
Box<String> stringBox = new Box<>();
stringBox.setInhalt("Hallo");
String text = stringBox.getInhalt(); // Kein Cast! Der Compiler weiß: Das ist ein String.
Box<Integer> intBox = new Box<>();
intBox.setInhalt(42);
Integer zahl = intBox.getInhalt(); // Kein Cast!
// Das geht NICHT:
stringBox.setInhalt(123); // COMPILE ERROR! int ist kein String
Warum ist das besser als ObjectBox?
- Typsicherheit:
stringBox.setInhalt(123)wird sofort als Fehler erkannt. - Kein Casting:
getInhalt()gibt direktStringzurück, nichtObject. - Lesbarkeit:
Box<String>sagt sofort, was drin ist. - IDE-Support: Autovervollständigung funktioniert!
Konventionen für Type Parameters:
| Parameter | Bedeutung | Beispiel |
|---|---|---|
T | Type (allgemein) | Box<T> |
E | Element (in Collections) | List<E> |
K | Key (in Maps) | Map<K, V> |
V | Value (in Maps) | Map<K, V> |
N | Number | Calculator<N> |
R | Return Type | Function<T, R> |
Diese Konventionen sind nicht vom Compiler erzwungen – du könntest auch Box<Banane> schreiben. Aber T, E, K, V sind Standard und machen deinen Code für andere Entwickler lesbar.
Der Diamond Operator <>
Seit Java 7 kannst du dir Tipparbeit sparen:
// Vor Java 7 – redundant List<String> namen = new ArrayList<String>(); Map<String, List<Integer>> kompliziert = new HashMap<String, List<Integer>>(); // Seit Java 7 – Diamond Operator List<String> namen = new ArrayList<>(); Map<String, List<Integer>> kompliziert = new HashMap<>(); // Viel besser!
Was passiert hier?
Der Compiler leitet den Typ aus dem Kontext ab. Da links List<String> steht, weiß er: rechts muss auch <String> sein. Das nennt man Type Inference.
Wichtig: Der Diamond <> ist NICHT dasselbe wie gar nichts:
List<String> a = new ArrayList<>(); // OK – Diamond, Typ wird inferiert List<String> b = new ArrayList(); // WARNING! Raw Type – keine Typsicherheit!
Generische Methoden
Du kannst auch einzelne Methoden generisch machen, ohne dass die ganze Klasse generisch sein muss:
public class ArrayUtils {
// Generische Methode – das <T> vor dem Rückgabetyp macht sie generisch
public static <T> T getFirst(List<T> liste) {
if (liste.isEmpty()) {
return null;
}
return liste.get(0);
}
// Noch ein Beispiel
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
}
Was macht dieser Code?
getFirst() nimmt eine Liste von irgendeinem Typ T und gibt das erste Element zurück – ebenfalls vom Typ T.
Wie funktioniert das im Detail?
Die Syntax <T> vor dem Rückgabetyp:
Bei generischen Methoden steht der Type Parameter vor dem Rückgabetyp:
public static <T> T getFirst(List<T> liste) // ^^^ ^ // | Rückgabetyp ist T // Type Parameter deklarieren
Type Inference bei Methodenaufrufen:
List<String> namen = List.of("Max", "Anna", "Tom");
String erster = ArrayUtils.getFirst(namen); // Compiler inferiert: T = String
List<Integer> zahlen = List.of(1, 2, 3);
Integer erste = ArrayUtils.getFirst(zahlen); // Compiler inferiert: T = Integer
Du musst den Typ nicht explizit angeben – der Compiler schaut sich das Argument an und leitet T daraus ab.
In der Praxis bedeutet das:
Du schreibst eine Methode einmal und sie funktioniert mit jedem Typ – typsicher.
🟡 PROFESSIONALS: Bounded Types & Wildcards
Jetzt wird’s interessant. Die Grundlagen waren „Generics verwenden“. Jetzt geht’s um „Generics designen“.
Bounded Type Parameters
Das Problem:
Du willst eine Methode, die eine Liste von Zahlen summiert:
// OHNE Bound – COMPILE ERROR!
public static <T> double sum(List<T> zahlen) {
double summe = 0;
for (T zahl : zahlen) {
summe += zahl.doubleValue(); // ERROR! T hat keine doubleValue()
}
return summe;
}
Warum funktioniert das nicht?
T könnte alles sein – String, Person, List<Banane>. Der Compiler kann nicht garantieren, dass T eine doubleValue()-Methode hat.
Die Lösung – Upper Bound:
// MIT Bound – T muss Number oder Subklasse sein
public static <T extends Number> double sum(List<T> zahlen) {
double summe = 0;
for (T zahl : zahlen) {
summe += zahl.doubleValue(); // OK! Number hat doubleValue()
}
return summe;
}
Was macht <T extends Number>?
Es sagt dem Compiler: „T ist nicht irgendwas. T ist garantiert Number oder eine Subklasse von Number (Integer, Double, BigDecimal, …).“
Wie funktioniert das im Detail?
Der Compiler prüft jeden Aufruf:
List<Integer> integers = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
List<String> strings = List.of("a", "b");
sum(integers); // OK – Integer extends Number ✓
sum(doubles); // OK – Double extends Number ✓
sum(strings); // COMPILE ERROR! String extends Number? Nein!
Das Prinzip dahinter:
extends bei Generics bedeutet „ist ein Subtyp von“ – das gilt für Klassen UND Interfaces:
// T muss Comparable implementieren
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Multiple Bounds:
// T muss Number UND Comparable sein
public static <T extends Number & Comparable<T>> T maxNumber(List<T> zahlen) {
// ... kann sowohl doubleValue() als auch compareTo() aufrufen
}
Wichtig: Bei Multiple Bounds muss die Klasse zuerst kommen, dann Interfaces:
- ✅
<T extends Number & Comparable<T>> - ❌
<T extends Comparable<T> & Number>– Compile Error!
Wildcards: ?, ? extends, ? super
Wildcards sind der Teil von Generics, der die meisten Entwickler verwirrt. Lass uns das ändern.
Das Szenario:
Du hast eine Methode, die eine Liste ausgibt:
public static void printList(List<Object> liste) {
for (Object obj : liste) {
System.out.println(obj);
}
}
// Verwendung:
List<String> namen = List.of("Max", "Anna");
printList(namen); // COMPILE ERROR! List<String> ist KEIN List<Object>!
Warum funktioniert das nicht?
Hier stolpern viele: String extends Object, also müsste doch List<String> auch ein List<Object> sein, oder?
Nein! Und das hat einen guten Grund:
// Stell dir vor, das würde kompilieren: List<String> strings = new ArrayList<>(); List<Object> objects = strings; // Hypothetisch erlaubt objects.add(42); // Das ist ein Object, also erlaubt? String s = strings.get(0); // BOOM! 42 ist kein String!
Generics sind invariant: List<String> ist KEIN Subtyp von List<Object>, auch wenn String ein Subtyp von Object ist.
Die Lösung – Wildcards:
1. Unbounded Wildcard: <?>
„Irgendein Typ – mir egal welcher.“
public static void printList(List<?> liste) {
for (Object obj : liste) { // Lesen als Object ist OK
System.out.println(obj);
}
}
printList(List.of("A", "B")); // OK
printList(List.of(1, 2, 3)); // OK
printList(List.of(new Person())); // OK
Was passiert hier?
List<?> akzeptiert eine Liste von jedem Typ. Aber es gibt einen Preis:
public static void addElement(List<?> liste) {
liste.add("Test"); // COMPILE ERROR!
liste.add(42); // COMPILE ERROR!
liste.add(null); // OK – null passt zu jedem Typ
}
Warum kann ich nichts hinzufügen?
Der Compiler weiß nicht, welchen Typ die Liste hat. Wenn es eine List<Integer> ist, wäre add("Test") falsch. Wenn es eine List<String> ist, wäre add(42) falsch. Also verbietet er beides.
2. Upper Bounded Wildcard: ? extends T
„T oder ein Subtyp von T.“
public static double summe(List<? extends Number> zahlen) {
double sum = 0;
for (Number n : zahlen) { // Lesen als Number ist OK
sum += n.doubleValue();
}
return sum;
}
summe(List.of(1, 2, 3)); // List<Integer> – OK
summe(List.of(1.5, 2.5)); // List<Double> – OK
summe(List.of("a", "b")); // COMPILE ERROR! String ist keine Number
Was macht ? extends Number?
Es sagt: „Die Liste enthält Numbers oder Subtypen davon.“ Du kannst also sicher Number-Methoden aufrufen.
Die Einschränkung:
List<? extends Number> zahlen = List.of(1, 2, 3); Number n = zahlen.get(0); // OK – lesen zahlen.add(42); // COMPILE ERROR! – schreiben verboten
Warum kann ich nicht schreiben?
zahlen könnte eine List<Integer> sein oder eine List<Double>. Wenn es eine List<Double> ist und du 42 (ein Integer) hinzufügst, bricht die Typsicherheit.
3. Lower Bounded Wildcard: ? super T
„T oder ein Supertyp von T.“
public static void addNumbers(List<? super Integer> liste) {
liste.add(1); // OK
liste.add(2); // OK
liste.add(3); // OK
}
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(integers); // OK – Integer ist Integer
addNumbers(numbers); // OK – Number ist Supertyp von Integer
addNumbers(objects); // OK – Object ist Supertyp von Integer
Was macht ? super Integer?
Es sagt: „Die Liste nimmt Integer an.“ Egal ob es eine List<Integer>, List<Number> oder List<Object> ist – Integer passt immer rein.
Die Einschränkung:
List<? super Integer> liste = new ArrayList<Number>(); liste.add(42); // OK – schreiben Object obj = liste.get(0); // OK – aber nur als Object! Integer i = liste.get(0); // COMPILE ERROR!
Warum bekomme ich nur Object zurück?
Der Compiler weiß nur, dass die Liste „Integer oder höher“ akzeptiert. Das könnte List<Object> sein. Also ist das Einzige, was er garantieren kann: Das Element ist mindestens ein Object.
PECS: Producer Extends, Consumer Super
Die Eselsbrücke, die du dir merken musst:
Producer Extends, Consumer Super
| Situation | Wildcard | Beispiel |
|---|---|---|
| Du liest aus der Collection (sie produziert Daten) | ? extends T | List<? extends Number> |
| Du schreibst in die Collection (sie konsumiert Daten) | ? super T | List<? super Integer> |
| Du liest UND schreibst | Keine Wildcard | List<T> |
Das klassische Beispiel – Collections.copy():
// Aus dem JDK – perfektes PECS
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
T element = src.get(i); // src = Producer → extends
dest.set(i, element); // dest = Consumer → super
}
}
Was passiert hier?
srcist ein Producer – wir lesen daraus →? extends Tdestist ein Consumer – wir schreiben hinein →? super T
In der Praxis:
List<Integer> integers = List.of(1, 2, 3); List<Number> numbers = new ArrayList<>(List.of(0.0, 0.0, 0.0)); Collections.copy(numbers, integers); // Funktioniert! // numbers enthält jetzt [1, 2, 3] als Number
Type Erasure – Was passiert zur Laufzeit?
Das wichtigste Konzept, das viele nicht kennen:
Generics existieren nur zur Compile-Zeit. Zur Laufzeit sind alle Typinformationen weg.
// Dein Code List<String> strings = new ArrayList<>(); List<Integer> integers = new ArrayList<>(); // Was die JVM sieht (nach Type Erasure) List strings = new ArrayList(); List integers = new ArrayList(); // Deshalb: System.out.println(strings.getClass() == integers.getClass()); // true!
Was macht Type Erasure?
Der Compiler:
- Prüft alle Typen zur Compile-Zeit
- Entfernt dann alle Typinformationen
- Fügt nötige Casts automatisch ein
Warum gibt es Type Erasure?
Backward Compatibility! Java 5 Generics mussten mit Java 1.4 Code kompatibel sein. Eine List<String> musste mit einer Library funktionieren, die noch List (ohne Generics) verwendet.
Konsequenzen:
// Das geht NICHT:
if (liste instanceof List<String>) { } // COMPILE ERROR!
T obj = new T(); // COMPILE ERROR!
T[] array = new T[10]; // COMPILE ERROR!
// Das geht:
if (liste instanceof List<?>) { } // OK – unbounded
if (liste instanceof List) { } // OK – raw type
Wichtig zu verstehen:
Type Erasure ist der Grund, warum du zur Laufzeit nicht fragen kannst „Ist das eine List<String>?“ Die Information ist schlicht nicht mehr da.
🔵 BONUS: Fortgeschrittene Patterns
Generische Klassen mit mehreren Type Parameters
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
}
Verwendung:
Pair<String, Integer> alter = new Pair<>("Max", 25);
Pair<Integer, Boolean> check = new Pair<>(42, true);
Pair<String, List<String>> komplex = new Pair<>("hobbies", List.of("Coding", "Gaming"));
Recursive Type Bounds
Für Typen, die sich selbst referenzieren:
// T muss mit sich selbst vergleichbar sein
public static <T extends Comparable<T>> T max(List<T> liste) {
if (liste.isEmpty()) {
throw new IllegalArgumentException("Liste ist leer");
}
T max = liste.get(0);
for (T element : liste) {
if (element.compareTo(max) > 0) {
max = element;
}
}
return max;
}
Warum Comparable<T> und nicht nur Comparable?
Comparable<T> stellt sicher, dass T mit sich selbst verglichen werden kann, nicht mit irgendetwas anderem.
Raw Types – Und warum du sie vermeiden solltest
Ein Raw Type ist eine generische Klasse ohne Typangabe:
List rawList = new ArrayList(); // Raw Type – WARNING!
rawList.add("String");
rawList.add(42);
rawList.add(new Object());
// Kompiliert – aber du verlierst ALLE Typsicherheit!
Wann Raw Types „akzeptabel“ sind:
instanceofChecks:if (obj instanceof List)- Class Literals:
List.class(nichtList<String>.class) - Legacy-Code Integration (aber dann bitte mit
@SuppressWarnings("unchecked"))
In neuem Code: Niemals Raw Types verwenden!
💬 Real Talk: „Ich versteh Wildcards einfach nicht“
Java Fleet Büro, Mittwoch 14:30. Nova starrt auf einen Compiler-Error.
Nova: „Elyndra, ich geb’s auf. Warum kann ich hier nichts hinzufügen?“
List<? extends Number> zahlen = getZahlen(); zahlen.add(42); // Error!
Elyndra: setzt sich dazu „Ah, die klassische Wildcard-Falle. Lass mich dir eine Frage stellen: Was gibt getZahlen() zurück?“
Nova: „Eine Liste von Numbers?“
Elyndra: „Genauer: Eine Liste von irgendeinem Subtyp von Number. Könnte List<Integer> sein. Könnte List<Double> sein. Könnte List<BigDecimal> sein.“
Nova: „Okay…“
Elyndra: „Und jetzt stell dir vor, getZahlen() gibt eine List<Double> zurück. Du willst 42 hinzufügen – das ist ein Integer. Was passiert?“
Nova: „Dann hätte ich einen Integer in einer Double-Liste…“
Elyndra: „Exakt. Das wäre ein Typfehler. Aber der Compiler weiß zur Compile-Zeit nicht, welcher konkrete Typ es ist. Also verbietet er ALLES außer null – sicherheitshalber.“
Jamal: kommt mit Kaffee vorbei „PECS, Nova. Producer Extends, Consumer Super.“
Nova: „Das hab ich schon hundertmal gehört, aber—“
Jamal: „Okay, anders erklärt: Wenn du aus einer Collection LIEST – sie produziert Daten für dich – dann extends. Wenn du in eine Collection SCHREIBST – sie konsumiert deine Daten – dann super.“
Elyndra: „Genau. List<? extends Number> ist wie ein Museum: Du darfst reinschauen, aber nichts anfassen. List<? super Integer> ist wie eine Spendenbüchse: Du darfst was reinwerfen, aber nicht reingucken was drin ist.“
Nova: „Oh. Das… macht tatsächlich Sinn.“
Jamal: „Und wenn du beides brauchst – lesen UND schreiben – dann keine Wildcard. Dann brauchst du einen konkreten Type Parameter.“
Nova: „Also List<T> statt List<?>?“
Elyndra: „Genau. Wildcards sind für Methoden-Parameter, wenn du flexibel sein willst. Type Parameters sind für Klassen oder wenn du den Typ mehrfach verwenden musst.“
❓ FAQ – Die Fragen, die wirklich kommen
„Was ist der Unterschied zwischen <T> und <?>?“
<T> ist ein benannter Type Parameter – du deklarierst ihn und kannst ihn verwenden:
public <T> T getFirst(List<T> liste) {
return liste.get(0); // Rückgabetyp ist T
}
<?> ist eine Wildcard – ein unbekannter Typ, den du nicht benennen kannst:
public void print(List<?> liste) {
// Du kannst den Typ nicht referenzieren
// Object ist das Beste was du bekommst
}
Faustregel: Brauchst du den Typ mehrfach oder als Rückgabe? → <T>. Brauchst du nur Flexibilität im Parameter? → <?>.
„Warum kann ich kein new T() schreiben?“
Wegen Type Erasure. Zur Laufzeit weiß die JVM nicht, was T ist. Der Aufruf new String() braucht die konkrete Klasse – aber die ist bei T nicht bekannt.
Workaround mit Class Token:
public class Factory<T> {
private final Class<T> type;
public Factory(Class<T> type) {
this.type = type;
}
public T create() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
}
// Verwendung:
Factory<StringBuilder> factory = new Factory<>(StringBuilder.class);
StringBuilder sb = factory.create();
„List<Object> und List<?> – was ist der Unterschied?“
List<Object> ist eine konkrete Liste von Objects:
List<Object> objects = new ArrayList<>();
objects.add("String"); // OK
objects.add(42); // OK
objects.add(null); // OK
Object o = objects.get(0); // OK
List<?> ist eine Liste von unbekanntem Typ:
List<?> unknown = getListeVonIrgendwo();
unknown.add("String"); // COMPILE ERROR!
unknown.add(42); // COMPILE ERROR!
unknown.add(null); // OK – null ist immer erlaubt
Object o = unknown.get(0); // OK – als Object lesen geht
„Warum List<Integer> und nicht List<int>?“
Generics funktionieren nur mit Referenztypen, nicht mit Primitives. Das liegt an Type Erasure – zur Laufzeit wird alles zu Object, und Primitives sind keine Objects.
List<Integer> zahlen = new ArrayList<>(); // OK – Autoboxing List<int> zahlen = new ArrayList<>(); // COMPILE ERROR!
„Bernd meint, Generics seien nur syntaktischer Zucker?“
seufz Bernd vereinfacht mal wieder. Ja, durch Type Erasure verschwinden die Typinfos zur Laufzeit. Aber Generics sind weit mehr als Syntax-Zucker:
- Compile-Zeit-Prüfung – Fehler werden früh gefunden
- Kein Casting nötig – sauberer, lesbarer Code
- Dokumentation –
Map<String, List<User>>sagt mehr alsMap - IDE-Support – Autovervollständigung funktioniert!
- Design-Tool – Generische APIs sind wiederverwendbar
🔍 Psst… du suchst tiefere Einblicke? Die Private Logs der Crew findest du, wenn du weißt wo du suchen musst…
🎨 Challenges: Praxis statt Fingerübungen
Keine abstrakten Pair<A,B>-Implementierungen. Diese Challenges basieren auf echten Problemen.
🟢 Level 1: Legacy-Code fixen (20-30 Min)
Szenario: Du übernimmst Code von einem Kollegen, der 2004 aufgehört hat zu lernen.
/**
* AUFGABE: Finde und behebe alle Generics-Probleme.
* Der Code kompiliert – aber er ist eine Zeitbombe!
*/
public class BestellService {
private List bestellungen = new ArrayList();
public void addBestellung(Object bestellung) {
bestellungen.add(bestellung);
}
public double berechneGesamtwert() {
double summe = 0;
for (Object obj : bestellungen) {
Bestellung b = (Bestellung) obj;
summe += b.getPreis();
}
return summe;
}
public List getBestellungenUeber(double minWert) {
List ergebnis = new ArrayList();
for (Object obj : bestellungen) {
Bestellung b = (Bestellung) obj;
if (b.getPreis() > minWert) {
ergebnis.add(b);
}
}
return ergebnis;
}
}
class Bestellung {
private String produkt;
private double preis;
public Bestellung(String produkt, double preis) {
this.produkt = produkt;
this.preis = preis;
}
public double getPreis() { return preis; }
public String getProdukt() { return produkt; }
}
Deine Aufgabe:
- Identifiziere alle Raw Types
- Füge Generics hinzu
- Entferne alle Casts
- Schreibe einen Test, der beweist dass dein Code typsicher ist
Bonus: Was passiert, wenn jemand addBestellung("Keine echte Bestellung") aufruft – vorher und nachher?
🟡 Level 2: Typsicherer Warenkorb (45-60 Min)
Szenario: Du baust einen Warenkorb für einen Online-Shop.
/**
* Alle Produkte im Shop müssen dieses Interface implementieren.
*/
public interface Purchasable {
double getPrice();
String getName();
String getCategory();
}
Deine Aufgabe – Implementiere ShoppingCart<T extends Purchasable>:
public class ShoppingCart<T extends Purchasable> {
// TODO: Datenstruktur für Items (mit Mengen!)
/**
* Fügt ein Item zum Warenkorb hinzu.
* Wenn das Item schon existiert, erhöhe die Menge.
*/
public void addItem(T item, int quantity) {
// TODO
}
/**
* Berechnet den Gesamtwert des Warenkorbs.
*/
public double getTotal() {
// TODO
}
/**
* Gibt alle Items unter einem bestimmten Preis zurück.
*
* FRAGE: Warum ist der Rückgabetyp List<T> und nicht List<? extends T>?
*/
public List<T> getItemsUnder(double maxPrice) {
// TODO
}
/**
* Kombiniert diesen Warenkorb mit einem anderen.
*
* HINWEIS: Hier brauchst du PECS!
* Der andere Warenkorb könnte Subtypen von T enthalten.
*/
public void mergeWith(ShoppingCart<? extends T> other) {
// TODO
}
}
Teste mit:
class Book implements Purchasable {
private String title;
private double price;
// ... Konstruktor, Getter
}
class Electronics implements Purchasable {
private String name;
private double price;
private int warrantyMonths;
// ... Konstruktor, Getter
}
// Test
ShoppingCart<Purchasable> cart = new ShoppingCart<>();
cart.addItem(new Book("Clean Code", 35.99), 1);
cart.addItem(new Electronics("USB-Kabel", 9.99, 12), 3);
Bonus-Fragen:
- Warum ist
ShoppingCart<? extends Purchasable>als Parameter fürmergeWithbesser alsShoppingCart<Purchasable>? - Könnte
getItemsUnderauchList<? extends T>zurückgeben? Was wäre der Unterschied?
🔵 Level 3: Report-Generator mit PECS (1-2 Std)
Szenario: Du schreibst einen Report-Generator für die HR-Abteilung.
public abstract class Employee {
protected String name;
protected String department;
protected double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// Getter...
}
public class Developer extends Employee {
private String programmingLanguage;
public Developer(String name, String department, double salary, String language) {
super(name, department, salary);
this.programmingLanguage = language;
}
public String getProgrammingLanguage() { return programmingLanguage; }
}
public class Manager extends Employee {
private int teamSize;
public Manager(String name, String department, double salary, int teamSize) {
super(name, department, salary);
this.teamSize = teamSize;
}
public int getTeamSize() { return teamSize; }
}
public class Intern extends Employee {
private String university;
public Intern(String name, String department, double salary, String university) {
super(name, department, salary);
this.university = university;
}
public String getUniversity() { return university; }
}
Deine Aufgabe – Implementiere den ReportGenerator:
public class ReportGenerator {
/**
* Exportiert eine Liste von Mitarbeitern nach CSV.
*
* HINWEIS: Die Methode soll mit List<Developer>, List<Manager>,
* und List<Employee> funktionieren. Welche Wildcard brauchst du?
*/
public void exportToCSV(List<____> employees, Path outputFile) throws IOException {
// Format: name,department,salary
// TODO
}
/**
* Importiert Developer aus einer CSV-Datei und fügt sie zur Zielliste hinzu.
*
* HINWEIS: Die Zielliste könnte List<Developer>, List<Employee>,
* oder List<Object> sein. Welche Wildcard brauchst du?
*/
public void importDevelopersFromCSV(List<____> target, Path inputFile) throws IOException {
// Liest CSV, erstellt Developer-Objekte, fügt sie zu target hinzu
// TODO
}
/**
* Filtert eine Liste nach Gehalt und schreibt das Ergebnis in eine Zielliste.
*
* HINWEIS: Hier brauchst du BEIDE Wildcards!
* source = Producer, target = Consumer
*/
public <T extends Employee> void filterBySalary(
List<____> source,
List<____> target,
double minSalary) {
// TODO
}
}
Test-Fragen (beantworte sie im Code als Kommentare):
exportToCSV(List<Developer>, ...)– funktioniert das? Warum?importDevelopersFromCSV(List<Employee>, ...)– funktioniert das? Warum?importDevelopersFromCSV(List<Intern>, ...)– funktioniert das? Warum NICHT?- In
filterBySalary: Warum brauchst du einen Type Parameter UND Wildcards?
🎁 Cheat Sheet
🟢 Basics
// Generische Klasse
public class Box<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
// Generische Methode
public static <T> T first(List<T> list) {
return list.get(0);
}
// Diamond Operator
List<String> list = new ArrayList<>();
🟡 Bounds & Wildcards
// Upper Bound <T extends Number> // T muss Number oder Subklasse sein <T extends Comparable<T>> // T muss Comparable implementieren // Multiple Bounds <T extends Number & Comparable<T>> // Wildcards List<?> // Irgendein Typ List<? extends Number> // Number oder Subklasse (LESEN) List<? super Integer> // Integer oder Superklasse (SCHREIBEN) // PECS – Producer Extends, Consumer Super void copy(List<? super T> dest, List<? extends T> src) // ^^^^^^^^^ Consumer ^^^^^^^^^ Producer
🔵 Advanced
// Mehrere Type Parameters
class Pair<K, V> { }
class Triple<A, B, C> { }
// Recursive Bounds
<T extends Comparable<T>>
// Class Token für new T()
public T create(Class<T> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
📦 Downloads
| Datei | Beschreibung | Download |
|---|---|---|
| tag03-generics-starter.zip | Starter mit TODOs | ⬇️ Download |
| tag03-generics-complete.zip | Musterlösung | ⬇️ Download |
Quick Start:
unzip tag03-generics-starter.zip cd tag03-generics-starter # In IntelliJ/Eclipse/NetBeans öffnen
🔗 Weiterführende Links
📚 Offizielle Dokumentation
| Ressource | Beschreibung |
|---|---|
| Oracle: Generics Tutorial | Der offizielle Einstieg |
| JLS: Generic Types | Die Spezifikation (für Hartgesottene) |
🛠️ Vertiefung
| Ressource | Beschreibung | Level |
|---|---|---|
| Baeldung: Java Generics | Praxisnahe Tutorials | 🟡 |
| Angelika Langer: Generics FAQ | DIE Referenz – 500+ Seiten | 🔴 |
| Effective Java, Item 26-33 | Best Practices von Josh Bloch | 🟡 |
🇩🇪 Deutsch
| Ressource | Beschreibung |
|---|---|
| Rheinwerk OpenBook: Generics | Umfassendes Kapitel |
| Java Blog Buch: Generics | Einsteigerfreundlich |
🎉 Tag 3 geschafft!
Du hast es geschafft! 🚀
Was du heute gelernt hast:
- ✅ Generics = Typsicherheit zur Compile-Zeit
- ✅ Type Parameters:
<T>,<E>,<K, V> - ✅ Bounded Types:
<T extends Number> - ✅ Wildcards:
?,? extends,? super - ✅ PECS: Producer Extends, Consumer Super
- ✅ Type Erasure und seine Konsequenzen
Challenges geschafft?
- 🟢 Level 1 → Du verstehst die Basics
- 🟡 Level 2 → Du kannst Generics designen
- 🔵 Level 3 → Du bist bereit für echte APIs
Wie geht’s weiter?
Morgen (Tag 4): Lambda-Ausdrücke – Eleganter Code, weniger Boilerplate
Heads up: Lambdas bauen auf dem auf, was du heute gelernt hast. Functional Interfaces wie Predicate<T>, Function<T, R> und Consumer<T> sind alle generisch!
📖 Kurs-Navigation
← Tag 2: Sets & Maps | Übersicht | Tag 4: Lambda-Ausdrücke →
Tags: #Java #Generics #TypeSafety #Wildcards #PECS #Tutorial
© 2025 Java Fleet Systems Consulting | java-developer.online

