- 1 Section
- 10 Lessons
- unbegrenzt
- Java Fortgeschritten10
- 1.1OOP in Java: Klassen, Kapselung, Konstruktoren
- 1.2Vererbung in Java
- 1.3Polymorphismus und Methodenüberschreibung
- 1.4Interfaces und abstrakte Klassen
- 1.5Collections: ArrayList, HashMap, LinkedList
- 1.6Generics und Typsicherheit
- 1.7Streams und Lambda-Ausdrücke
- 1.8Dateioperationen: IO und NIO
- 1.9Unit-Tests in Java: JUnit 5
- 1.10Praxisprojekt: Anwendung mit OOP und Tests
Generics und Typsicherheit
Bei den Collections in L5 sind sie ständig aufgetaucht: die spitzen Klammern <String> oder <Integer>. Das sind Generics – eine der wichtigsten Spracherweiterungen, die Java seit Version 5 (2004) hat.
Generics erlauben dir, mit Typ-Parametern zu arbeiten: eine Klasse oder Methode wird einmal geschrieben, funktioniert aber typsicher mit beliebigen Typen. Statt jede Liste neu zu schreiben (StringList, IntegerList, …) hast du eine generische List<T>, die der Compiler für jeden konkreten Typ spezialisiert. Diese Lektion zeigt, wie das funktioniert, was Wildcards (?) machen und wo die Grenzen liegen.
1) Warum Generics?
Stell dir vor, Java hätte keine Generics. Eine Liste würde alles aufnehmen – aber beim Auslesen müsstest du jedes Element casten und hoffen, dass es passt:
List namen = new ArrayList(); namen.add("Anna"); namen.add(42); // kompiliert! String n = (String) namen.get(1); // ClassCastException zur Laufzeit
List<String> namen = new ArrayList<>(); namen.add("Anna"); namen.add(42); // Compile-FEHLER String n = namen.get(1); // kein Cast nötig, kein Laufzeitfehler
Generics machen Fehler vom Laufzeit-Problem zum Compile-Problem – fängt sie also schon beim Bauen ab, nicht erst bei einem unzufriedenen Nutzer.
2) Eine generische Klasse selbst schreiben
So baust du eine eigene generische Klasse. Beispiel: ein simpler „Behälter", der genau einen Wert von beliebigem Typ aufnimmt:
public class Behaelter<T> { private T inhalt; public void setInhalt(T inhalt) { this.inhalt = inhalt; } public T getInhalt() { return inhalt; } }
Nutzung mit verschiedenen Typen:
Behaelter<String> bString = new Behaelter<>(); bString.setInhalt("Hallo"); String s = bString.getInhalt(); // kein Cast Behaelter<Integer> bInt = new Behaelter<>(); bInt.setInhalt(42); int i = bInt.getInhalt(); // Auto-Unboxing
Das T in der Klassendefinition ist ein Typ-Parameter. Beim Verwenden ersetzt du es durch einen konkreten Typ wie String oder Integer. Der Compiler erzeugt daraus die typsichere Variante.
3) Konventionen für Typ-Parameter
Typ-Parameter sind technisch beliebige Namen, aber es gibt eine etablierte Konvention:
List<T>, Optional<T>.Collection<E>, Iterator<E>.Map<K,V>, Map.Entry<K,V>.Function<T,R>.NumberFormat<N>.Halte dich an die Konvention, dann liest dein Code sich für andere Java-Entwickler natürlich.
4) Mehrere Typ-Parameter
Eine Klasse kann mehrere Typ-Parameter haben – klassisches Beispiel: ein Paar aus zwei verschiedenen Typen:
public class Paar<A, B> { private A erstes; private B zweites; public Paar(A erstes, B zweites) { this.erstes = erstes; this.zweites = zweites; } public A getErstes() { return erstes; } public B getZweites() { return zweites; } } // Nutzung Paar<String, Integer> p = new Paar<>("Alter", 28); System.out.println(p.getErstes() + ": " + p.getZweites());
Die Standardbibliothek nutzt das ständig: Map<K,V>, Function<T,R>, BiFunction<T,U,R>.
5) Generische Methoden
Du kannst nicht nur ganze Klassen, sondern auch einzelne Methoden generisch machen – die Typ-Parameter stehen dann vor dem Rückgabetyp:
public class Utils { // Generische statische Methode public static <T> T letztes(List<T> liste) { if (liste.isEmpty()) return null; return liste.get(liste.size() - 1); } } // Nutzung – Typ wird automatisch erkannt String last = Utils.letztes(List.of("a", "b", "c")); // "c" Integer i = Utils.letztes(List.of(1, 2, 3)); // 3
Der Compiler leitet den Typ aus dem Argument ab – das nennt man Type Inference. Du musst <T> nicht explizit beim Aufruf angeben (kannst es aber).
6) Bounded Type Parameters
Manchmal willst du den Typ-Parameter einschränken: „T muss eine Zahl sein" oder „T muss vergleichbar sein". Das geht mit extends:
// T muss Number oder eine Unterklasse von Number sein public static <T extends Number> double summe(List<T> zahlen) { double total = 0; for (T z : zahlen) { total += z.doubleValue(); // Number kennt doubleValue() } return total; } // Funktioniert mit allen Zahlen Utils.summe(List.of(1, 2, 3)); // 6.0 Utils.summe(List.of(1.5, 2.5)); // 4.0 Utils.summe(List.of("a")); // Compile-Fehler
Mit dem Bound T extends Number garantiert der Compiler, dass z.doubleValue() immer existiert. Das funktioniert auch für Interfaces:
public static <T extends Comparable<T>> T max(List<T> liste) { T maximum = liste.get(0); for (T x : liste) { if (x.compareTo(maximum) > 0) maximum = x; } return maximum; }
7) Wildcards – das Fragezeichen
Manchmal weißt du den Typ nicht genau. Statt eines konkreten Typ-Parameters benutzt du dann das Wildcard ?:
List<?> akzeptiert List<String>, List<Integer> usw. Nur Lesen, kein Schreiben (außer null).List<? extends Number> akzeptiert List<Integer>, List<Double>. Producer – zum Lesen.List<? super Integer> akzeptiert List<Number>, List<Object>. Consumer – zum Schreiben.Konkrete Beispiele:
// Liest aus einer Liste irgendwelcher Zahlen public static double summe(List<? extends Number> liste) { double total = 0; for (Number n : liste) total += n.doubleValue(); return total; } // Schreibt Integer in eine Liste, die Integer aufnehmen kann public static void fuelle(List<? super Integer> liste, int n) { for (int i = 0; i < n; i++) liste.add(i); }
8) PECS-Regel
Wann nimmt man welche Wildcard? Die Eselsbrücke heißt PECS: Producer Extends, Consumer Super.
- Du liest aus der Sammlung („sie produziert Werte für dich") →
? extends T - Du schreibst in die Sammlung („sie konsumiert deine Werte") →
? super T - Du tust beides → konkreter Typ ohne Wildcard
Die PECS-Regel ist in IHK-Klausuren selten, in der täglichen Java-Praxis aber sehr wichtig – etwa beim Lesen von Funktionsdokumentationen.
9) Generics in Interfaces
Interfaces können generisch sein – Beispiel aus der Standardbibliothek:
public interface Comparable<T> { int compareTo(T anderes); } public class Artikel implements Comparable<Artikel> { private double preis; @Override public int compareTo(Artikel anderer) { return Double.compare(this.preis, anderer.preis); } }
Die Klasse muss bei der Implementierung den konkreten Typ angeben (Comparable<Artikel>). Ergebnis: compareTo bekommt direkt einen Artikel, keinen Object mit nachträglichem Cast.
10) Type Erasure – die Kehrseite
Generics in Java haben eine wichtige Eigenheit: zur Laufzeit sind sie nicht mehr da. Der Compiler entfernt sie nach der Typprüfung – das nennt man Type Erasure:
List<String> strings = new ArrayList<>(); List<Integer> ints = new ArrayList<>(); System.out.println(strings.getClass() == ints.getClass()); // true – zur Laufzeit sind beide einfach ArrayList
Konsequenzen:
instanceof List<String>geht nicht: zur Laufzeit ist der Typparameter weg. Erlaubt ist nurinstanceof List- Arrays mit Generics sind verboten:
new List<String>[10]kompiliert nicht. Workaround:List<List<String>> - Statische Felder mit Typ-Parameter geht nicht:
private static T x;in einer KlasseMeineKlasse<T>ist illegal - Keine Reflection mit Typ-Parameter:
T.classexistiert zur Laufzeit nicht
List<Hund> ist kein Subtyp von List<Tier>, auch wenn Hund Subtyp von Tier ist. Wenn du das brauchst, nutze Wildcards: List<? extends Tier>.
11) Raw Types – Altlasten
Vor Java 5 gab es keine Generics – nur List, ohne Typ-Parameter. Aus Kompatibilitätsgründen ist das auch heute noch erlaubt:
List liste = new ArrayList(); // Raw Type – warnt liste.add("a"); liste.add(42); List<String> typisiert = liste; // Unchecked-Warning
Der Compiler warnt davor (unchecked), kompiliert aber. In neuem Code: niemals Raw Types nutzen. Sie umgehen alle Typsicherheits-Vorteile. Wenn du wirklich „irgendein Typ" brauchst, nimm List<?> – das ist explizit und sicher.
12) Diamond-Operator
Seit Java 7 darfst du den Typ rechts vom new weglassen – der Compiler liest ihn aus der linken Seite ab:
// Alt (Java 5/6) List<String> alt = new ArrayList<String>(); // Neu (Java 7+) – Diamond-Operator List<String> neu = new ArrayList<>(); Map<String, List<Integer>> m = new HashMap<>();
Kürzer, gleich sicher. Bei modernen Versionen nutzt man fast immer den Diamond-Operator.
13) Generics + var (Java 10+)
Mit var kannst du den Variablen-Typ weglassen, wenn er aus dem Rechtsterm ableitbar ist:
var liste = new ArrayList<String>(); // Typ: ArrayList<String>, ableitbar aus dem new var wirr = new ArrayList<>(); // Typ: ArrayList<Object> – wahrscheinlich nicht das, was du wolltest
var ist bequem, aber bei generischen Typen vorsichtig sein – ohne explizite Typ-Angabe rechts wird oft Object daraus.
14) Typische Anwendungsfälle
Wo nutzt du Generics aktiv selbst?
- Eigene Collection-artige Klassen: Caches, Pools, Container, Wrappers
- Repository-Pattern:
UserRepository implements Repository<User, Long> - Builder-Pattern:
StringBuilder,StreamBuilder<T> - Funktional-Programmierung:
Function<T,R>,Predicate<T>,Consumer<T> - Result-Typen:
Optional<T>, eigeneResult<T, E> - Frameworks: Spring's
JpaRepository<Entity, ID>, Jackson'sTypeReference<T>
15) Häufige Fehler
- Raw Type benutzt:
ListstattList<String>– Compiler warnt, du verlierst Typsicherheit - Generic-Array erzeugen wollen:
new T[10]geht nicht – nutzeList<T> - Kovarianz erwartet:
List<Hund>ist keinList<Tier>– stattdessenList<? extends Tier> - Type Erasure ignoriert:
if (x instanceof List<String>)kompiliert nicht - Bounded Type vergessen: ohne
T extends Comparablekannst du nicht.compareTonutzen - Wildcards überall:
?nur, wenn der konkrete Typ wirklich egal ist. Sonst lieber konkrete Typ-Parameter
<T> bei einer Klasse?", „Was leistet eine generische Collection im Vergleich zu Raw Type?". Wildcards (?) und PECS sind selten Stoff, aber Praxis-relevant.
Zusammenfassung
Generics erlauben typsichere, wiederverwendbare Klassen und Methoden mit Typ-Parametern (T, E, K,V, …). Sie verschieben Typprüfungen von der Laufzeit zur Compile-Zeit und machen Casts überflüssig. Eine eigene generische Klasse definierst du mit class Behaelter<T>; eine generische Methode mit <T> vor dem Rückgabetyp. Mit Bounded Type Parameters (T extends Number) schränkst du erlaubte Typen ein. Wildcards: ? für „beliebig", ? extends T zum Lesen (Producer), ? super T zum Schreiben (Consumer) – die PECS-Regel. Type Erasure: Generics existieren nur zur Compile-Zeit, zur Laufzeit sind sie weg. Generics sind nicht kovariant: List<Hund> ist kein List<Tier>. Raw Types (ohne Typ-Parameter) sind Altlast und sollten vermieden werden.
