1. Invarianz von Generics: List<Number> ≠ List<Integer>
In Java sind Generics streng: Sie sind invariant. Das bedeutet, dass selbst wenn ein Typ ein Subtyp eines anderen ist (zum Beispiel ist Integer ein Subtyp von Number), Collections mit diesen Typen nicht miteinander kompatibel sind.
Stellen Sie sich vor: Sie haben eine Kiste mit Äpfeln (List<Integer>), und jemand behauptet, das sei überhaupt eine „Kiste mit Obst“ (List<Number>). Klingt logisch – Äpfel sind ja Früchte. Aber dann könnte man in diese Kiste auch eine Banane (Double) legen, und alles würde kaputtgehen – tatsächlich ist es eine „Apfelkiste“.
Codebeispiel:
List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // Kompilierfehler!
Der Compiler ist hier absichtlich streng: Er lässt nicht zu, eine Liste mit „Äpfeln“ in eine Liste mit „Obst“ zu verwandeln. Andernfalls könnten wir alles Mögliche hinzufügen, und das Programm würde erst zur Laufzeit abstürzen.
Deshalb sind List<Number> und List<Integer> zwei völlig unterschiedliche Typen, obwohl Integer für sich genommen ein Subtyp von Number ist.
Im Gegensatz zu Arrays:
Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // Erlaubt (Arrays sind kovariant)
numArr[0] = 3.14; // Laufzeitfehler (ArrayStoreException)
Fazit: Generics sind invariant, um Typsicherheit bereits zur Kompilierzeit zu gewährleisten.
2. Grenzen der Typparameter
Manchmal muss man einschränken, mit welchen Typen eine generische Klasse oder Methode arbeiten darf. Dafür verwendet man Bounds (Grenzen).
Obere Grenze (extends)
class Stats<T extends Number> {
private T[] nums;
// ...
}
Jetzt kann Stats nur mit Typen arbeiten, die von Number erben (Integer, Double, Float usw.). Der Versuch, Stats<String> zu erstellen, führt zu einem Kompilierfehler.
Einschränkung mit Interface
class Sorter<T extends Comparable<T>> {
void sort(List<T> list) { /* ... */ }
}
Jetzt kann Sorter nur mit Typen arbeiten, die das Interface Comparable implementieren.
Mehrfache Grenzen
Man kann mehrere Grenzen mit & angeben:
class MyClass<T extends Number & Comparable<T>> { /* ... */ }
T muss eine Unterklasse von Number und Comparable<T> implementieren.
Die Reihenfolge ist wichtig: zuerst die Klasse, dann die Interfaces.
3. Einführung in Wildcards: ?
Eine Wildcard ist ein „Platzhalter“-Typ, der sagt: „Hier kann irgendein Typ stehen, aber ich weiß nicht, welcher genau.“
Beispiele:
- List<?> – eine Liste von irgendetwas.
- List<? extends Number> – eine Liste eines beliebigen Typs, der eine Unterklasse von Number ist (z. B. Integer, Double).
- List<? super Integer> – eine Liste eines beliebigen Typs, der ein Supertyp von Integer ist (z. B. Integer, Number, Object).
Wozu braucht man Wildcards?
Sie ermöglichen es, Methoden zu schreiben, die mit Collections verschiedener, aber verwandter Typen arbeiten.
Beispiel: nur lesen (Producer)
void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
// list.add(123); // Fehler! Elemente dürfen nicht hinzugefügt werden
}
- Man kann Elemente als Number lesen.
- Nicht erlaubt, Elemente hinzuzufügen (außer null).
Beispiel: nur schreiben (Consumer)
void addIntegers(List<? super Integer> list) {
list.add(42); // OK
// Integer x = list.get(0); // Fehler! Wir wissen nicht, welcher Typ zurückkommt
}
- Man kann Elemente vom Typ Integer (oder dessen Subtypen) hinzufügen.
- Nicht möglich, Elemente sicher als Integer zu lesen (nur als Object).
PECS-Regel
PECS – „Producer Extends, Consumer Super“:
- Producer – Extends: wenn die Collection nur Daten liefert (Producer), verwenden Sie ? extends T.
- Consumer – Super: wenn die Collection nur Daten entgegennimmt (Consumer), verwenden Sie ? super T.
Merke: extends – zum Lesen (Producer), super – zum Schreiben (Consumer).
Vergleich: Arrays sind kovariant, Generics invariant
- Arrays sind kovariant: Integer[] kann einer Variablen vom Typ Number[] zugewiesen werden.
- Generics sind invariant: List<Integer> kann nicht einer Variablen vom Typ List<Number> zugewiesen werden.
Wildcards können die Invarianz von Generics teilweise „abmildern“.
5. Generische Methoden und Typinferenz; Einschränkungen (type erasure)
Generische Methoden
Man kann generische Methoden deklarieren, die mit beliebigen Typen arbeiten:
public static <T> void printList(List<T> list) {
for (T elem : list) {
System.out.println(elem);
}
}
Typinferenz
Java kann den Typparameter „erraten“:
List<String> strings = List.of("a", "b");
printList(strings); // T = String
Einschränkungen von Generics: Typlöschung
- In Java sind Generics mittels Typlöschung implementiert: Informationen über Typparameter werden nach der Kompilierung entfernt.
- Im Bytecode gibt es keinen Unterschied zwischen List<String> und List<Integer>.
- Man kann keine Arrays generischer Typen erstellen: new List<String>[10] – Fehler.
- Man kann instanceof nicht mit Typparametern verwenden: obj instanceof List<String> – Fehler.
6. Praxis mit Collections und Stream-API
Elemente zwischen Listen kopieren
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item);
}
}
- src – Producer (? extends T)
- dest – Consumer (? super T)
Verwendung:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number ist ein Supertyp von Integer
Stream-API und Wildcards
List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;
numbers.stream()
.map(Number::doubleValue)
.forEach(System.out::println);
Filtern mit Wildcard
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
7. Häufige Fehler
Fehler Nr. 1: Raw Types (Rohtypen).
Die Verwendung von Rohtypen schaltet die Typprüfung ab und führt zu Laufzeitfehlern.
List list = new ArrayList(); // raw type – schlecht!
list.add("Zeichenkette");
list.add(123); // Man kann alles Mögliche hinzufügen
String s = (String) list.get(1); // ClassCastException!
Verwenden Sie niemals Raw Types. Geben Sie immer Typparameter an: List<String>, List<Integer>.
Fehler Nr. 2: Unsichere Operationen mit extends.
Der Versuch, Elemente zu einer Collection mit ? extends ... hinzuzufügen, führt zu einem Kompilierfehler.
List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // Kompilierfehler!
Fehler Nr. 3: „Typlöschung“ bei Überladung.
Methoden dürfen nicht nur anhand generischer Parameter überladen werden – nach Typlöschung sind die Signaturen identisch.
public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // Kompilierfehler!
Fehler Nr. 4: Arrays generischer Typen.
Aufgrund der Typlöschung können keine Arrays parametrisierter Typen erstellt werden.
List<String>[] arr = new List<String>[10]; // Kompilierfehler!
GO TO FULL VERSION