Zum Inhalt springen
Java Anfänger 30 min

Generics Grundlagen

Generics in Java verstehen: Typsichere Collections, eigene generische Klassen und Methoden, Wildcards und Bounded Types.

Aktualisiert:

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

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

SituationWildcardMerksatz
Lesen aus Collection? extends TProducer extends
Schreiben in Collection? super TConsumer super
Lesen und SchreibenKein WildcardKonkreter 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!

Zurück zum Java Kurs