RU | EN | DE

Warum Generics?

  • Typsicherheit zur Kompilierungszeit (weniger ClassCastException).
  • Code-Wiederverwendung für verschiedene Typen.
  • API-Dokumentation durch Map<String, Integer> ist verständlicher als Map.

Basissyntax: <T>, <E>, <K, V>

Klasse/Interface mit Typparameter

public class Box<T> {
  private T value;
  public Box(T value) { this.value = value; }
  public T get() { return value; }
  public void set(T value) { this.value = value; }
}
Box<String> sb = new Box<>("hi");
String s = sb.get();  // sicher, ohne Cast

Mehrere Parameter

public class Pair<K, V> {
  private final K key; private final V value;
  public Pair(K key, V value) { this.key = key; this.value = value; }
  public K key() { return key; } public V value() { return value; }
}

Generische Methoden

public static <T> T first(List<T> list) {
  return list.isEmpty() ? null : list.get(0);
}

Beschränkungen (Bounds)

public class NumberBox<T extends Number> { /* ... */ }
// mehrere Beschränkungen:
public static <T extends Number & Comparable<T>> T max(T a, T b) {
  return a.compareTo(b) >= 0 ? a : b;
}

Wildcards: ?, ? extends, ? super + Prinzip PECS

Invarianz: Der Schlüssel zum Verständnis

List<Integer> ist nicht weder List<Number>, noch ein Subtyp, noch ein Supertyp.
Arrays sind covariant (Number[] arr = new Integer[10]), Generics sind invariant.

Wildcard ?

List<?> ist eine Liste eines unbekannten Typs: man kann es als Object lesen, aber man kann nichts hinzufügen (außer null).

Obere Grenze: ? extends T (“Producer”)

void printNums(List<? extends Number> src) {
  // kann als Number gelesen werden
  Number n = src.get(0);
  // src.add(42); // nicht möglich – der genaue Typ ist unbekannt
}

Untere Grenze: ? super T (“Consumer”)

void addIntegers(List<? super Integer> dst) {
  dst.add(1);  // kann Integer (und seine Subtypen) hinzufügen
  // Integer x = dst.get(0); // gibt Object zurück, unsichere Lektüre
}

Das Goldene PECS-Regel

  • Producer Extends: Wenn man liest (Quelle), verwendet man ? extends.
  • Consumer Super: Wenn man schreibt (Empfänger), verwendet man ? super.
Klassisches Beispiel: Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }

Quelle – extends, Empfänger – super.

Häufige Fragen und Fallen

Warum List<Object>List<String>?

Invarianz: Andernfalls könnte man Integer in List<String> über einen List<Object>-Link einfügen.
Verwenden Sie List<?>, wenn man eine “beliebige Liste nur zum Lesen” benötigt.

Was unterscheidet List<? extends Number> von List<Number>?

  • In List<Number> kann man Integer, Long, Double usw. hinzufügen.
  • In List<? extends Number> kann man nicht hinzufügen (der genaue Typ ist unbekannt), aber man kann ihn sicher als Number lesen.

Wozu ist List<? super Integer> nützlich?

Das ist ein “Eimer” zum Schreiben von Objekten vom Typ Integer. Man kann von dort nur als Object lesen.

Wildcard-Erfassung (Wildcard Capture)

Manchmal benötigt man einen “Helfer”, um mit ? zu arbeiten:

static void swapFirst(List<?> list) { swapHelper(list); }
private static <T> void swapHelper(List<T> list) {
  if (list.size() > 1) {
  T tmp = list.get(0);
  list.set(0, list.get(1));
  list.set(1, tmp);
  }
}

Type Inference (Typableitung)

“Diamant” (diamond) ab Java 7+

Map<String, List<Integer>> m = new HashMap<>();

Ableitung in generischen Methoden

var x = List.of(1, 2, 3);  // List<Integer>
var y = first(List.of("a"));  // String

Manchmal explizit <T> bei der Aufruf verwenden: MyClass.<Long>first(...).

Type Erasure (Typentupfung)

Was ist das: Typparameter existieren nur zur Kompilierungszeit.
Die JVM sieht “rohe” Typen: List<T>List. Daher Einschränkungen:

Konsequenzen der Typentupfung

  1. Man kann keinen Array eines parametrisierten Typs erstellen:
List<String>[] arr = new List<String>[10]; // Fehler
  1. Man kann keine new T() und T.class verwenden:
T t = new T();  // Fehler
Class<T> c = T.class; // Fehler

Umgehung: Class<T> in den Konstruktor/Methode übergeben:

class Factory<T> {
  private final Class<T> type;
  Factory(Class<T> type) { this.type = type; }
  T create() throws Exception { return type.getDeclaredConstructor().newInstance(); }
}
  1. Man kann Methoden nicht überschreiben, die sich nur in den Typparametern unterscheiden:
void foo(List<String> x) {}
void foo(List<Integer> y) {} // gleiche Erradikation → Konflikt
  1. instanceof nur mit Wildcard oder rohen Typen:
if (obj instanceof List<?>) { /* ok */ }
// if (obj instanceof List<String>) // Kompilierungsfehler
  1. Bridge-Methoden: Der Compiler generiert sie, um den Polymorphismus bei der Typentupfung zu erhalten (oft im Bytecode/Debug sichtbar).
  2. Reifiable vs non-reifiable:
  • Reifiable (erhalten Infos zur Laufzeit): List<?>, List, Array von primitiven Typen.
  • Non-reifiable: List<String>, Map<Integer, String> – der Typparameter wird entfernt.

Type Token / Typ speichern

Benutzen “Typträger”:

abstract class TypeRef<T> {
  final Type type = ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];
}
TypeRef<List<String>> ref = new TypeRef<>() {};
System.out.println(ref.type); // java.util.List<java.lang.String>

Generics und Arrays/Varargs

  • Arrays sind covariant und werden zur Laufzeit überprüft; Generics sind invariant und werden zur Kompilierungszeit überprüft.
  • Varargs mit Generics führen zu einer Warnung heap pollution.
    Markieren Sie die Methode @SafeVarargs (nur static/final/private) und mischen Sie keine internen Details:
@SafeVarargs
public static <T> List<T> asList(T... items) { return Arrays.asList(items); }

Praktische Signaturen für Interviews

  • Maximaler Comparator
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
  if (list.isEmpty()) return null;
  T best = list.get(0);
  for (T t : list) if (t.compareTo(best) > 0) best = t;
  return best;
}
  • Kopieren aus Quelle in Empfänger (PECS)
public static <T> void copyAll(List<? super T> dest, List<? extends T> src) {
  for (T t : src) dest.add(t);
}
  • Allgemeiner Repository im Spring-Stil
public interface Repository<T extends BaseEntity, ID> {
  T save(T entity);
  Optional<T> findById(ID id);
}

Typische Fehler

  • ❌ Rohe Typen (List list) → verlieren Sicherheit, es kommt zu ClassCastException.
  • ❌ Rückgabe von List<? extends T> aus einer API-Methode. Besser List<T> zurückgeben; Wildcards in den Argumenten, nicht in den Rückgabetypen.
  • ❌ Missbrauch von ? extends dort, wo man schreiben sollte. Denken Sie an PECS.
  • ❌ Versuchen, Methoden zu überschreiben, die sich nur in den Typparametern unterscheiden (Typentupfung wird es brechen).
  • List<Object> statt List<?> für “beliebige Liste nur zum Lesen”.

Best Practices

  • Halten Sie sich an PECS: extends – lesen, super – schreiben.
  • Verwenden Sie Wildcards in den Parametern für öffentliche APIs, nicht in den Rückabetypen.
  • Vermeiden Sie rohe Typen; falls nötig, isolieren Sie sie und kommentieren Sie @SuppressWarnings("unchecked") mit einem Kommentar.
  • Verwenden Sie sinvolle Namen für Typparameter: T, E, K, V, ID, R, U.
  • Für Factorys/Reflexion verwenden Sie Class<T> oder den Typenträger (type token), nicht new T().

Mini-Worterbuch der Unterschiede (zum Merken)

  • List<?> – kann als Object gelesen, nicht geschrieben.
  • List<? extends T> – kann als T gelesen, nicht geschrieben.
  • List<? super T> – kann schreiben T, lesen als Object.
  • Generics sind invariant; Arrays sind covariant.
  • Typentupfung: kein new T(), instanceof List<String> und Überschreibungen “nur über Generics”.