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

TagThemaLevelStatus
1Collections – Listen🟢✅ Abgeschlossen
2Collections – Sets & Maps🟢✅ Abgeschlossen
→ 3Generics – Typsicherheit🟡👉 DU BIST HIER
4Lambda-Ausdrücke🟡🔜 Kommt als nächstes
5Functional Interfaces🟡🔒
6Stream-API🟡🔒
7File I/O🟡🔒
8Annotations & Multithreading🟡🔒
9Multithreading – Synchronisation🔴🔒
10Netzwerkprogrammierung🔴🔒

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 LevelEmpfohlenes Tempo
🌱 Neu dabei1 Tag pro Tag – lies alles, mach jede Übung
🌿 Kannst schon programmierenÜberspring GRUNDLAGEN wenn sie klar sind
🌳 ProfiSpring 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 nur List ist

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?

  1. List<String> sagt dem Compiler: „Diese Liste enthält NUR Strings.“
  2. Der Compiler prüft jeden add()-Aufruf.
  3. namen.add(42) wird sofort als Fehler markiert – nicht erst zur Laufzeit.
  4. 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?

  1. Typsicherheit: stringBox.setInhalt(123) wird sofort als Fehler erkannt.
  2. Kein Casting: getInhalt() gibt direkt String zurück, nicht Object.
  3. Lesbarkeit: Box<String> sagt sofort, was drin ist.
  4. IDE-Support: Autovervollständigung funktioniert!

Konventionen für Type Parameters:

ParameterBedeutungBeispiel
TType (allgemein)Box<T>
EElement (in Collections)List<E>
KKey (in Maps)Map<K, V>
VValue (in Maps)Map<K, V>
NNumberCalculator<N>
RReturn TypeFunction<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

SituationWildcardBeispiel
Du liest aus der Collection (sie produziert Daten)? extends TList<? extends Number>
Du schreibst in die Collection (sie konsumiert Daten)? super TList<? super Integer>
Du liest UND schreibstKeine WildcardList<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?

  • src ist ein Producer – wir lesen daraus → ? extends T
  • dest ist 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:

  1. Prüft alle Typen zur Compile-Zeit
  2. Entfernt dann alle Typinformationen
  3. 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:

  • instanceof Checks: if (obj instanceof List)
  • Class Literals: List.class (nicht List<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
  • DokumentationMap<String, List<User>> sagt mehr als Map
  • 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:

  1. Identifiziere alle Raw Types
  2. Füge Generics hinzu
  3. Entferne alle Casts
  4. 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:

  1. Warum ist ShoppingCart<? extends Purchasable> als Parameter für mergeWith besser als ShoppingCart<Purchasable>?
  2. Könnte getItemsUnder auch List<? 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):

  1. exportToCSV(List<Developer>, ...) – funktioniert das? Warum?
  2. importDevelopersFromCSV(List<Employee>, ...) – funktioniert das? Warum?
  3. importDevelopersFromCSV(List<Intern>, ...) – funktioniert das? Warum NICHT?
  4. 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

DateiBeschreibungDownload
tag03-generics-starter.zipStarter mit TODOs⬇️ Download
tag03-generics-complete.zipMusterlösung⬇️ Download

Quick Start:

unzip tag03-generics-starter.zip
cd tag03-generics-starter
# In IntelliJ/Eclipse/NetBeans öffnen

🔗 Weiterführende Links

📚 Offizielle Dokumentation

RessourceBeschreibung
Oracle: Generics TutorialDer offizielle Einstieg
JLS: Generic TypesDie Spezifikation (für Hartgesottene)

🛠️ Vertiefung

RessourceBeschreibungLevel
Baeldung: Java GenericsPraxisnahe Tutorials🟡
Angelika Langer: Generics FAQDIE Referenz – 500+ Seiten🔴
Effective Java, Item 26-33Best Practices von Josh Bloch🟡

🇩🇪 Deutsch

RessourceBeschreibung
Rheinwerk OpenBook: GenericsUmfassendes Kapitel
Java Blog Buch: GenericsEinsteigerfreundlich

🎉 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

Autoren

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.

  • Ensign Nova Trent

    24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.

  • Jamal Hassan

    ⚙️ Jamal Hassan – Der Zuverlässige

    Backend Developer | 34 Jahre | „Ich schau mir das an.“

    Wenn im Team jemand gebraucht wird, der ruhig bleibt, während alle anderen hektisch diskutieren, dann ist es Jamal.
    Er redet nicht viel – er löst.
    Er plant wie ein Schachspieler: drei Züge im Voraus, jede Entscheidung mit Folgen bedacht.
    Seine Art zu arbeiten ist kein Sprint, sondern eine Strategie.

    Er kam 2021 zur Java Fleet, nachdem sein vorheriges Startup gescheitert war. Statt Frust hat er Gelassenheit mitgebracht – und eine Haltung, die das Team bis heute prägt: Stabilität ist keine Bremse, sondern ein Fundament.
    In einer Welt voller Hypes baut Jamal Systeme, die bleiben.

    💻 Die Tech-Seite

    Jamal ist der Inbegriff von Backend-Handwerk.
    Er liebt Architektur, die logisch ist, Datenmodelle, die Bestand haben, und Services, die einfach laufen.
    Spring Boot, REST, Kafka, Docker, DDD – das sind seine Werkzeuge, aber nicht sein Selbstverständnis.
    Er versteht Systeme als Ökosysteme: Jede Entscheidung hat Auswirkungen, jedes Modul muss sich in das Ganze einfügen.

    Er ist der Typ Entwickler, der eine halbe Stunde in Stille auf den Bildschirm schaut – und dann mit einem Satz alles löst:

    „Das Problem liegt nicht im Code. Es liegt in der Annahme.“

    Sein Code ist wie seine Persönlichkeit: still, präzise, verlässlich.
    Er dokumentiert, was nötig ist, und schreibt Tests, weil er Verantwortung ernst nimmt.
    Er hält nichts von Schnellschüssen – und noch weniger von Ausreden.

    🌿 Die menschliche Seite

    Jamal ist kein Mensch der Bühne.
    Er mag es, wenn andere glänzen – Hauptsache, das System läuft.
    Er trinkt arabischen Kaffee, spielt Schach im Verein und genießt es, wenn Dinge logisch ineinandergreifen – egal ob Code oder Leben.
    In der Kaffeeküche hört man ihn selten, aber wenn er etwas sagt, ist es meist ein Satz, der hängen bleibt.

    Im Team ist er der stille Vertraute, der Probleme anhört, bevor er sie bewertet.
    Nova nennt ihn „den Debugger in Menschengestalt“, Kat sagt: „Wenn Jamal nickt, weißt du, dass du auf der richtigen Spur bist.“
    Und Cassian beschreibt ihn als „Architekt mit Geduld und ohne Ego“.

    🧠 Seine Rolle im Team

    Jamal ist das strukturelle Rückgrat der Crew.
    Er denkt in Systemen, nicht in Features – in Verantwortlichkeiten, nicht in Ruhm.
    Wenn Projekte drohen, aus dem Ruder zu laufen, bringt er sie mit wenigen Worten und einer klaren Architektur wieder auf Kurs.
    Er ist das, was Franz-Martin „den ruhigen Hafen im Sturm“ nennt.

    ⚡ Superkraft

    Stabilität durch Denken.
    Jamal löst nicht nur technische Probleme – er beseitigt deren Ursachen.

    ☕ Motto

    „Ich schau mir das an.“
    (Und wenn er das sagt, ist das Problem so gut wie gelöst.)