Generics Grundlagen
Generics in Java verstehen: Typsichere Collections, eigene generische Klassen und Methoden, Wildcards und Bounded Types.
Generics Grundlagen
Generics sind eines der maechtigsten Features in Java. Sie ermoglichen dir, Klassen und Methoden zu schreiben, die mit verschiedenen Typen arbeiten — und trotzdem typsicher sind. Du hast sie bereits benutzt: List<String>, Map<String, Integer> — das <String> ist ein Generic!
Warum Generics?
Ohne Generics (vor Java 5)
// Ohne Generics: Alles ist Object
List liste = new ArrayList();
liste.add("Hallo");
liste.add(42); // Alles rein -- keine Typpruefung!
String text = (String) liste.get(0); // Cast noetig!
String text2 = (String) liste.get(1); // ClassCastException zur Laufzeit!
Mit Generics
// Mit Generics: Typsicher!
List<String> liste = new ArrayList<>();
liste.add("Hallo");
// liste.add(42); // Kompilierfehler! Nur Strings erlaubt.
String text = liste.get(0); // Kein Cast noetig!
Generics verlagern den Fehler von der Laufzeit (ClassCastException) in die Kompilierzeit (Kompilierfehler). Das ist viel sicherer!
Generics verwenden
Collections mit Generics
// List von Strings
List<String> namen = new ArrayList<>();
namen.add("Max");
// Set von Ganzzahlen
Set<Integer> zahlen = new HashSet<>();
zahlen.add(42);
// Map von String zu Double
Map<String, Double> noten = new HashMap<>();
noten.put("Mathe", 2.3);
// Liste von eigenen Typen
record Student(String name, double note) {}
List<Student> studenten = new ArrayList<>();
studenten.add(new Student("Max", 1.7));
Der Diamond-Operator <>
// Ausfuehrlich:
List<String> namen = new ArrayList<String>();
// Kurz (Diamond-Operator):
List<String> namen = new ArrayList<>(); // Java erkennt den Typ
// Mit var:
var namen = new ArrayList<String>(); // Typ muss rechts stehen
Eigene generische Klassen
Du kannst eigene Klassen mit Generics erstellen:
public class Box<T> {
private T inhalt;
public Box(T inhalt) {
this.inhalt = inhalt;
}
public T getInhalt() {
return inhalt;
}
public void setInhalt(T inhalt) {
this.inhalt = inhalt;
}
@Override
public String toString() {
return "Box[" + inhalt + "]";
}
}
var stringBox = new Box<>("Hallo");
var intBox = new Box<>(42);
var studentBox = new Box<>(new Student("Max", 1.7));
System.out.println(stringBox.getInhalt()); // Hallo (String)
System.out.println(intBox.getInhalt()); // 42 (Integer)
Typparameter-Konventionen
| Parameter | Bedeutung | Beispiel |
|---|---|---|
T | Type (allgemeiner Typ) | Box<T> |
E | Element (in Collections) | List<E> |
K | Key (Schluessel) | Map<K, V> |
V | Value (Wert) | Map<K, V> |
N | Number | Calculator<N> |
R | Return (Rueckgabe) | Function<T, R> |
Mehrere Typparameter
public class Paar<A, B> {
private final A erstes;
private final B zweites;
public Paar(A erstes, B zweites) {
this.erstes = erstes;
this.zweites = zweites;
}
public A getErstes() { return erstes; }
public B getZweites() { return zweites; }
@Override
public String toString() {
return "(" + erstes + ", " + zweites + ")";
}
}
var paar = new Paar<>("Max", 25);
System.out.println(paar); // (Max, 25)
System.out.println(paar.getErstes()); // Max (String)
System.out.println(paar.getZweites()); // 25 (Integer)
Oder als Record (noch kuuerzer):
record Paar<A, B>(A erstes, B zweites) {}
var paar = new Paar<>("Max", 25);
Generische Methoden
Auch einzelne Methoden koennen generisch sein:
public class Utils {
// Generische Methode
static <T> void drucke(T[] array) {
for (var element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// Generische Methode mit Rueckgabewert
static <T> T erstesElement(List<T> liste) {
if (liste.isEmpty()) {
return null;
}
return liste.get(0);
}
// Zwei Typparameter
static <K, V> void druckeMap(Map<K, V> map) {
map.forEach((k, v) -> System.out.println(k + " -> " + v));
}
}
Integer[] zahlen = {1, 2, 3};
String[] namen = {"Max", "Anna"};
Utils.drucke(zahlen); // 1 2 3
Utils.drucke(namen); // Max Anna
String erster = Utils.erstesElement(List.of("A", "B", "C")); // "A"
Integer erstZahl = Utils.erstesElement(List.of(1, 2, 3)); // 1
Bounded Types (Begrenzte Typen)
Upper Bound: extends
Mit extends beschraenkst du den Typ auf eine bestimmte Klasse oder deren Unterklassen:
// T muss Number oder eine Unterklasse sein (Integer, Double, etc.)
public class MathBox<T extends Number> {
private T wert;
public MathBox(T wert) {
this.wert = wert;
}
public double alsDouble() {
return wert.doubleValue(); // Number-Methode verfuegbar!
}
public boolean istPositiv() {
return wert.doubleValue() > 0;
}
}
var intBox = new MathBox<>(42); // OK: Integer extends Number
var doubleBox = new MathBox<>(3.14); // OK: Double extends Number
// var stringBox = new MathBox<>("Hallo"); // FEHLER! String ist kein Number
Mehrere Bounds
// T muss Comparable UND Serializable sein
public class SortierBox<T extends Comparable<T> & java.io.Serializable> {
private T wert;
public SortierBox(T wert) {
this.wert = wert;
}
public boolean istGroesserAls(T anderer) {
return wert.compareTo(anderer) > 0;
}
}
Wildcards
Unbeschraenkte Wildcard: ?
static void drucke(List<?> liste) {
for (var element : liste) {
System.out.println(element);
}
}
drucke(List.of("A", "B")); // OK
drucke(List.of(1, 2, 3)); // OK
drucke(List.of(true, false)); // OK
Upper Bounded Wildcard: ? extends T
“Liste von irgendetwas, das T oder eine Unterklasse ist” — zum Lesen:
static double summe(List<? extends Number> zahlen) {
var ergebnis = 0.0;
for (var zahl : zahlen) {
ergebnis += zahl.doubleValue();
}
return ergebnis;
}
summe(List.of(1, 2, 3)); // OK: List<Integer>
summe(List.of(1.1, 2.2, 3.3)); // OK: List<Double>
Lower Bounded Wildcard: ? super T
“Liste von irgendetwas, das T oder eine Oberklasse ist” — zum Schreiben:
static void fuelleGanzzahlen(List<? super Integer> liste) {
liste.add(1);
liste.add(2);
liste.add(3);
}
var intListe = new ArrayList<Integer>();
var numListe = new ArrayList<Number>();
fuelleGanzzahlen(intListe); // OK
fuelleGanzzahlen(numListe); // OK
PECS-Regel: Producer Extends, Consumer Super
| Situation | Wildcard | Merksatz |
|---|---|---|
| Lesen aus Collection | ? extends T | Producer extends |
| Schreiben in Collection | ? super T | Consumer super |
| Lesen und Schreiben | Kein Wildcard | Konkreter Typ |
Praktisches Beispiel: Generische Stapel-Klasse
public class Stapel<T> {
private final List<T> elemente = new ArrayList<>();
public void push(T element) {
elemente.add(element);
}
public T pop() {
if (istLeer()) {
throw new RuntimeException("Stapel ist leer!");
}
return elemente.remove(elemente.size() - 1);
}
public T oben() {
if (istLeer()) {
throw new RuntimeException("Stapel ist leer!");
}
return elemente.get(elemente.size() - 1);
}
public boolean istLeer() {
return elemente.isEmpty();
}
public int groesse() {
return elemente.size();
}
@Override
public String toString() {
return "Stapel" + elemente;
}
}
var zahlenStapel = new Stapel<Integer>();
zahlenStapel.push(1);
zahlenStapel.push(2);
zahlenStapel.push(3);
System.out.println(zahlenStapel.pop()); // 3
System.out.println(zahlenStapel); // Stapel[1, 2]
var stringStapel = new Stapel<String>();
stringStapel.push("Hallo");
stringStapel.push("Welt");
Uebungen
Uebung 1: Generischer Container
Schreibe eine generische Klasse Container<T>, die einen Wert speichert und Methoden get(), set(), istVorhanden() und leeren() hat.
Uebung 2: Generische Suche
Schreibe eine generische Methode <T> int finde(List<T> liste, T element), die den Index eines Elements zurueckgibt (-1 wenn nicht gefunden).
Uebung 3: Maximum finden
Schreibe eine generische Methode <T extends Comparable<T>> T maximum(List<T> liste), die das groesste Element findet.
Uebung 4: Paar-Klasse erweitern
Erweitere die Paar-Klasse um eine Methode tausche(), die ein neues Paar mit vertauschten Elementen zurueckgibt: Paar<B, A>.
Was kommt als Naechstes?
In der naechsten Lektion starten wir mit Exception Handling — dem Umgang mit Fehlern in Java. Du lernst, was Exceptions sind und wie du mit Checked und Unchecked Exceptions umgehst.
Zusammenfassung
- Generics machen Code typsicher, ohne ihn fuer jeden Typ neu zu schreiben
<T>definiert einen Typparameter,<>(Diamond) laesst Java den Typ erkennen- Bounded Types (
<T extends Number>) schraenken den erlaubten Typ ein - Wildcards (
?,? extends T,? super T) erhoehen die Flexibilitaet - PECS: Producer extends, Consumer super
- Generics existieren nur zur Kompilierzeit (Type Erasure) — zur Laufzeit sind sie weg
- Konventionen:
T(Type),E(Element),K(Key),V(Value)
Pro-Tipp: Du musst die PECS-Regel und Wildcards nicht sofort meistern — das kommt mit der Erfahrung. Fuer den Anfang reicht es, Generics mit konkreten Typen zu verwenden: List<String>, Map<String, Integer>. Wenn IntelliJ dir einen Wildcard-Vorschlag macht, probiere es aus und schaue, was passiert. So lernst du am besten!