Inhalt

Generische Datentypen in Java

1. Theorie: Generische Datentypen

Generische Datentypen (Generics) erlauben es, Klassen, Interfaces und Methoden mit Platzhaltern für Datentypen zu definieren. Dadurch kann derselbe Code für unterschiedliche Typen wiederverwendet werden – bei gleichzeitig besserer Typsicherheit.

1.1 Warum Generics?

Ohne Generics wurden Sammlungen oft mit dem Typ Object verwendet. Beim Auslesen musste dann mit Type-Cast gearbeitet werden, was zu Laufzeitfehlern führen kann.

Mit Generics:

1.2 Grundsyntax

Ein generischer Typ-Parameter wird in spitzen Klammern angegeben:

class Box<T> {
    private T value;

    public void set(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }
}

Verwendung:

Box<String> textBox = new Box<>();
textBox.set("Hallo");

Box<Integer> numberBox = new Box<>();
numberBox.set(42);

1.3 Typ-Parameter in Java

Häufige Konventionen:

Mehrere Typ-Parameter sind möglich, z. B. Map<K, V>.

1.4 Grenzen von Generics (kurz)

Beispiel zu Type Erasure

Quellcode mit Generics:

List<String> names = new ArrayList<>();
names.add("Anna");
String first = names.get(0);

Vereinfacht gesagt behandelt der Compiler das intern nach der Typauslöschung so:

List names = new ArrayList();
names.add("Anna");
String first = (String) names.get(0);

Das heißt: Der Generic-Typ String ist zur Laufzeit nicht mehr als eigener Typ in der Liste vorhanden; die Typsicherheit wurde bereits beim Kompilieren geprüft.

2. Beispiele mit ArrayList

Die Klasse ArrayList<E> ist ein Standardbeispiel für Generics:

import java.util.ArrayList;

ArrayList<String> names = new ArrayList<>();
names.add("Anna");
names.add("Lukas");

String first = names.get(0); // kein Cast nötig

2.1 Beispiel mit Zahlen

ArrayList<Integer> values = new ArrayList<>();
values.add(10);
values.add(20);
values.add(30);

int sum = 0;
for (Integer value : values) {
    sum += value;
}
System.out.println("Summe: " + sum);

2.2 Beispiel mit eigenen Klassen

class Student {
    String name;
    int semester;

    Student(String name, int semester) {
        this.name = name;
        this.semester = semester;
    }
}

ArrayList<Student> students = new ArrayList<>();
students.add(new Student("Mila", 2));
students.add(new Student("Noah", 4));

for (Student s : students) {
    System.out.println(s.name + " (" + s.semester + ". Semester)");
}

2.3 Wildcards

Wildcards werden verwendet, wenn der genaue Typ einer generischen Struktur nicht exakt festgelegt werden soll.

Merksatz (PECS):

2.3.1 Ungebundene Wildcard ?

List<?> kann Elemente sicher lesen (als Object), aber nicht gezielt neue Werte hinzufügen (außer null).

public static void printAnyList(List<?> values) {
    for (Object value : values) {
        System.out.println(value);
    }
}

2.3.2 Unterklassen mit ? extends

Mit ? extends Number akzeptiert die Methode Listen von Integer, Double, Long usw.

public static double sum(List<? extends Number> numbers) {
    double result = 0;
    for (Number n : numbers) {
        result += n.doubleValue();
    }
    return result;
}

List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);

System.out.println(sum(ints));
System.out.println(sum(doubles));

Hinweis: Bei List<? extends Number> sollte man keine neuen Zahlen per add(...) einfügen, weil der konkrete Untertyp unbekannt ist.

2.3.3 Oberklassen mit ? super

Mit ? super Integer kann eine Methode Integer-Werte sicher einfügen. Als Zieltyp sind dann z. B. List<Integer>, List<Number> oder List<Object> möglich.

public static void addDefaults(List<? super Integer> target) {
    target.add(10);
    target.add(20);
}

List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addDefaults(numbers);
addDefaults(objects);

Wichtig: Beim Auslesen aus List<? super Integer> erhält man nur Object, da der konkrete Typ oberhalb von Integer liegen kann.

2.3.4 Warum oft List statt ArrayList?

In Methoden und Variablendeklarationen wird häufig das Interface List verwendet, nicht die konkrete Klasse ArrayList. Grund: Der Code bleibt flexibler, weil später auch andere Implementierungen wie LinkedList verwendet werden können, ohne die Methodensignatur zu ändern.

List<Number> values = new ArrayList<>();

Hier ist die konkrete Instanz weiterhin eine ArrayList, aber nach außen wird nur der allgemeinere Typ List benötigt.

Aufgaben

1. Theoriefragen

Beantworte kurz in eigenen Worten:

  1. Welches Problem lösen Generics?
  2. Warum ist ArrayList<String> sicherer als eine Liste mit Object?
  3. Was bedeutet der Typ-Parameter T?
  4. Was ist mit Type Erasure gemeint?

2. Erste generische Klasse

Implementiere eine Klasse Box<T> mit folgenden Methoden:

Teste die Klasse mit:

3. Eigene Objekte in ArrayList

Erstelle eine Klasse Produkt mit:

Lege eine ArrayList<Produkt> an und füge mindestens 5 Produkte hinzu. Gib anschließend alle Produkte formatiert aus.

4. Generische Utility-Methode

Implementiere eine generische Methode:

public static <T> void printList(List<T> list)

Die Methode soll alle Elemente der Liste mit Index ausgeben. Teste die Methode mit:

5. Vertiefung mit Wildcards

Implementiere eine Methode:

public static double average(List<? extends Number> numbers)

Die Methode soll den Durchschnitt berechnen. Teste sie mit:

6. Key+Value

Erstelle eine generische Klasse Pair<K, V>, die zwei Werte verwaltet (z. B. Schlüssel/Wert). Teste sie mit unterschiedlichen Typkombinationen, z. B.:

7. Wildcards mit Unterklassen (extends)

Gegeben sind die Listen:

Implementiere die Methode:

public static double maxValue(List<? extends Number> values)

Anforderungen:

8. Wildcards mit Oberklassen (super)

Implementiere die Methode:

public static void fillWithSequence(List<? super Integer> target, int n)

Anforderungen:

Algorithmen und Datenstrukturen