Java Generische Methoden
Methoden mit eigenen Typparametern in Java definieren, unabhängig von der umgebenden Klasse.
Eine generische Methode ist eine Methode, die in ihrer Signatur einen eigenen Typparameter einführt, unabhängig von Parametern auf Klassenebene. Dies ist das richtige Werkzeug, wenn die Typbeziehung nur zu einer einzigen Methode gehört — ein Hilfsprogramm, das zwei Array-Elemente tauscht, eine Fabrik, die eine Liste mit dem zurückgibt, was der Aufrufer übergibt, oder ein statischer Helfer ohne eine Instanz, an die man ein T anhängen könnte. Generische Methoden sind der Weg, auf dem fast jede statische Hilfsmethode in java.util.Collections und java.util.Arrays geschrieben ist.
Wo der Typparameter steht
Der Typparameter wird vor dem Rückgabetyp deklariert, zwischen den Modifikatoren und dem Rückgabetyp:
public static <T> T identity(T value) {
return value;
}Von links nach rechts gelesen: „public, static, deklariert einen Typparameter T, gibt ein T zurück, heißt identity, nimmt ein T entgegen." Das <T> ist es, was diese Methode zu einer generischen Methode macht – im Gegensatz zu einer Methode, die zufällig ein T der Klasse verwendet.
Aufgerufen wird sie wie eine normale Methode — der Compiler leitet das Typargument aus den übergebenen Argumenten ab:
String s = identity("hello"); // T inferred as String
Integer n = identity(42); // T inferred as IntegerFalls die Ableitung fehlschlägt oder man sie übersteuern möchte, kann man das Typargument explizit mit der Type-Witness-Syntax nach dem Punkt angeben:
String s = MyUtil.<String>identity("hello"); // rarely neededIn zehn Jahren Java wird man diese explizite Form vielleicht ein Dutzend Mal schreiben.
Warum ein methodenebener Parameter statt einem auf Klassenebene
Ein Parameter auf Klassenebene sagt: „Diese gesamte Klasse dreht sich um einen Typ." Ein Parameter auf Methodenebene sagt: „Diese eine Operation ist polymorph in einem Typ, der den Aufruf nicht überleben muss." Sie sind keine Alternativen füreinander — sie beantworten unterschiedliche Fragen:
// Method-level: the class isn't generic; the method is.
public class Arrays {
public static <T> void swap(T[] arr, int i, int j) { ... }
}
// Class-level: the class is parameterised; methods share that T.
public class Box<T> {
public T get() { ... }
public void set(T value) { ... }
}Einen methodenebenen Parameter verwenden, wenn:
- Die Methode
staticist (sie hat keine Instanz, also keinTauf Klassenebene zum Ausleihen). - Die Typbeziehung lokal zur Methode ist — Eingabe und Ausgabe teilen sich einen Typ, aber die Klasse nicht.
- Verschiedene Aufrufe derselben Methode unterschiedliche Typen verwenden sollen:
swapauf einemString[]undswapauf einemInteger[]sollen beide funktionieren, ohne dass sich die Klasse auf einen festlegen muss.
Mehrere Typparameter in einer Methode
Die gleiche Regel gilt: Diese werden zwischen den Modifikatoren und dem Rückgabetyp deklariert, durch Kommas getrennt:
public static <K, V> Map.Entry<K, V> entry(K key, V value) {
return new AbstractMap.SimpleImmutableEntry<>(key, value);
}
Map.Entry<String, Integer> e = entry("Ada", 100);Sowohl K als auch V werden aus den Argumenten abgeleitet. Wenn die beiden Parameter zufällig denselben Typ teilen, ist der abgeleitete Typ derjenige, auf den sich beide Argumente einigen:
public static <T> T firstOf(T a, T b) { return a; }
firstOf("x", "y"); // T = String
firstOf("x", 42); // T = Object — the closest common supertypeDas letzte Beispiel ist manchmal eine Falle. Der Compiler lehnt es nicht ab; er erweitert T stillschweigend auf Object. Wenn man „zwei Argumente desselben exakten Typs" möchte, können Generics das nach der Erweiterung nicht erzwingen — man müsste die Argumente zu separaten Typparametern machen.
Ein methodenebener Parameter in einer generischen Klasse
Eine generische Klasse kann generische Methoden haben, die eigene Parameter einführen, die sich von denen der Klasse unterscheiden. Die beiden Parameter koexistieren:
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
// U is local to this method — independent of T.
public <U> Box<U> map(java.util.function.Function<T, U> fn) {
return new Box<>(fn.apply(value));
}
}
Box<String> name = new Box<>("Ada");
Box<Integer> length = name.map(String::length); // T=String, U=IntegerDas <U> von map ist nur innerhalb von map gültig. Es kann T verwenden (weil es sich innerhalb von Box<T> befindet), kann es aber nicht ersetzen.
Typableitung in der Praxis
Der Compiler leitet die Typparameter einer Methode ab aus:
- Den Typen der expliziten Argumente.
- Dem Zieltyp — dem, was man dem Ergebnis zuweist, oder dem Parametertyp einer Methode, in die man das Ergebnis übergibt.
Die zweite Quelle erklärt, warum List.of(), Collections.emptyList() und ähnliche rückgabewert-basierte Generics meistens ohne expliziten Type-Witness funktionieren:
List<String> empty = Collections.emptyList(); // T inferred from the left side
process(Collections.emptyList()); // T inferred from `process`'s parameterWenn keine der beiden Quellen verfügbar ist (keine Argumente, kein Zieltyp), fällt der Compiler auf Object zurück. Das ist fast nie das, was man möchte — man schreibt den Type-Witness oder fügt einen Zieltyp hinzu:
var x = Collections.emptyList(); // List<Object> — probably not what you meant
List<String> y = Collections.emptyList(); // List<String> ✓Eine reale Form: Hilfsmethoden auf Collections
Die Collections.unmodifiableList, Collections.sort, Collections.shuffle der Standardbibliothek und ähnliche sind allesamt generische Methoden auf einer nicht-generischen Hilfsklasse. sort in seinem Grundprinzip:
public static <T extends Comparable<T>> void sort(List<T> list) {
// ... sorts using natural order
}Diese Signatur macht zwei Dinge gleichzeitig. Das <T> deklariert einen Typparameter. Das extends Comparable<T> ist eine Schranke — T muss ein Typ sein, der weiß, wie er sich mit sich selbst vergleicht. Wir werden ein ganzes Kapitel mit begrenzten Parametern verbringen; bemerken Sie jetzt einfach, dass die Schranke es der Methode ermöglicht, compareTo auf ihren Elementen aufzurufen.
Ein ausgearbeitetes Beispiel: ein typisiertes Swap, ein typisiertes Last, ein typisiertes Map
Eine kleine Hilfsklasse mit drei generischen Methoden — eine ohne Rückgabewert, eine, die denselben Typ zurückgibt, den sie entgegennahm, und eine, die elementweise auf einen neuen Typ abbildet. Zusammen decken sie die drei Formen ab, die man am häufigsten schreiben wird.
Drei Dinge sind zu beachten. swap funktioniert sowohl auf String[] als auch auf Integer[], weil T pro Aufruf abgeleitet wird. last gibt den Elementtyp zurück, den der Aufrufer übergeben hat — kein Cast auf der empfangenden Seite. map führt zwei Typparameter ein und verbindet sie über den Parameter Function<T, R> — der Compiler stellt sicher, dass die Funktion den Elementtyp der Liste entgegennimmt und den Elementtyp der Ergebnisliste zurückgibt.
Was kommt als Nächstes
Sie haben die beiden Arten gesehen, einen Typparameter zu deklarieren — auf einer Klasse und auf einer Methode. Der nächste Schritt ist der dritte Ort, an dem ein Typparameter stehen kann: auf einem Interface. So definiert die Standardbibliothek List<E>, Comparator<T>, Function<T, R> und jeden anderen Vertrag, den man implementiert, wenn man polymorphen Code schreibt. Weiter zu Java Generic Interfaces.