Java Methodenreferenzen
Methoden als Lambdas in Java mit dem ::-Operator referenzieren — statisch, instanzgebunden, ungebunden und Konstruktorreferenzen.
Eine Methodenreferenz ist eine kürzere Syntax für ein Lambda, dessen Körper nichts anderes tut, als eine bestehende Methode aufzurufen. Wenn ein Lambda buchstäblich x -> SomeClass.foo(x) oder (a, b) -> a.bar(b) lautet, ermöglicht der :: Operator, es als SomeClass::foo oder Some::bar zu schreiben. Der Compiler erzeugt in beiden Fällen denselben Wert — eine Instanz des entsprechenden funktionalen Interface — daher passt überall, wo ein Lambda passt, auch eine entsprechende Methodenreferenz.
Function<String, Integer> len1 = s -> s.length();
Function<String, Integer> len2 = String::length; // identical at runtime
List<String> names = List.of("Bob", "Alice");
names.forEach(s -> System.out.println(s)); // lambda
names.forEach(System.out::println); // method referenceDie vier folgenden Formen decken jede Methodenreferenz ab, die Sie schreiben werden. Die einzige Fähigkeit besteht darin, zu erkennen, welche Form an einer bestimmten Aufrufstelle passt.
Form 1: Statische Methodenreferenz — ClassName::staticMethod
Die Methode ist eine static-Methode einer Klasse. Die Referenz wird zu einem Lambda, dessen Parameter die Parameter der statischen Methode sind:
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
BinaryOperator<Integer> max = Math::max; // (a, b) -> Math.max(a, b)
Function<Object, String> toStr = String::valueOf; // o -> String.valueOf(o)Dies ist die Form, die in Stream-Code wie nums.stream().reduce(0, Integer::sum) auftaucht — Integer.sum(int, int) ist statisch, daher ist Integer::sum ein BinaryOperator<Integer> (ein BiFunction<Integer, Integer, Integer>).
Form 2: Gebundene Instanzmethodenreferenz — instance::method
Die Methode ist eine Instanzmethode eines bestimmten, benannten Objekts. Die Referenz wird zu einem Lambda, dessen Parameter die Parameter der Methode sind (die Instanz wird erfasst):
PrintStream out = System.out;
Consumer<String> print = out::println; // s -> out.println(s)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat; // name -> prefix.concat(name)
List<String> log = new ArrayList<>();
Consumer<String> record = log::add; // msg -> log.add(msg)Der gebundene Empfänger besetzt den argumentlosen Slot: Da prefix bereits erfasst ist, benötigt greet nur das Argument für concat und ist daher eine Function<String, String> statt einer BiFunction. Ebenso hält log::add log fest und stellt nur das hinzuzufügende Element frei, was einen Consumer<String> ergibt.
Die erfasste instance wird vom resultierenden Objekt gehalten, ähnlich wie ein Lambda effectively final-Locals erfasst. Gebundene Referenzen sind die Art, wie man sagt "verwende die Methode dieses Objekts als Callback" — Logger::info für einen bestimmten Logger, event::handle für einen bestimmten Handler.
Form 3: Ungebundene Instanzmethodenreferenz — ClassName::method
Die Methode ist eine Instanzmethode, aber Sie referenzieren sie über die Klasse statt über eine bestimmte Instanz. Die Referenz wird zu einem Lambda, dessen erster Parameter der Empfänger ist und die übrigen Parameter die eigenen Parameter der Methode sind:
Function<String, Integer> len = String::length; // s -> s.length() — first param is the receiver
Function<String, String> upper = String::toUpperCase; // s -> s.toUpperCase()
BiPredicate<String, String> starts = String::startsWith; // (s, prefix) -> s.startsWith(prefix)Dies ist die Form, über die Menschen stolpern. String::length sieht aus, als könnte es "die Methode length der Klasse String" bedeuten — aber es gibt keine solche statische Methode. Es bedeutet wirklich "gegeben einem beliebigen String, rufe seine Instanzmethode length() auf — der Empfänger ist der erste Parameter des Lambdas." Deshalb ist String::length eine Function<String, Integer> (eine Eingabe, eine Ausgabe) und String::startsWith ein BiPredicate<String, String> (die zweite Eingabe ist das Präfix, das der Empfänger testet).
Diese Form ist der Motor hinter fast jeder Stream-Pipeline:
people.stream()
.map(Person::name) // unbound: p -> p.name()
.filter(s -> s.startsWith("A"))
.map(String::toUpperCase) // unbound: s -> s.toUpperCase()
.forEach(System.out::println); // bound: s -> out.println(s)Form 4: Konstruktorreferenz — ClassName::new
Referenziert einen Konstruktor als Funktion. Das resultierende Lambda nimmt die Konstruktorparameter und gibt eine neue Instanz zurück:
Supplier<List<String>> listOf = ArrayList::new; // () -> new ArrayList<>()
Function<Integer, ArrayList<?>> sized = ArrayList::new; // n -> new ArrayList<>(n)
Function<String, BigDecimal> toBig = BigDecimal::new; // s -> new BigDecimal(s)
BiFunction<String, Integer, AbstractMap.SimpleEntry<String, Integer>> entry =
AbstractMap.SimpleEntry::new;Konstruktorreferenzen sind der Grund, warum Collectors.toCollection(TreeSet::new) Ihnen die Wahl des Zieltyps ermöglicht und Stream.generate(Random::new) pro Aufruf von get() unabhängige Random-Objekte erzeugt.
Arrays haben eine besondere Form: String[]::new ist eine IntFunction<String[]> — n -> new String[n]. Dies ist das, was stream.toArray(String[]::new) verwendet.
Methodenreferenz vs. Lambda — wann was besser passt
Eine Methodenreferenz ist die richtige Wahl, wenn der Körper des Lambdas genau ein einzelner Methodenaufruf ist, bei dem die Parameter in der richtigen Reihenfolge übergeben werden:
| Lambda | Methodenreferenz |
|---|---|
s -> s.length() | String::length |
s -> System.out.println(s) | System.out::println |
(a, b) -> a.compareTo(b) | String::compareTo |
() -> new ArrayList<>() | ArrayList::new |
Ein Lambda ist die richtige Wahl, wenn der Körper irgendetwas anderes tut:
- Ruft mehr als eine Methode auf:
s -> s.trim().toUpperCase()(keine Referenz für die Kette). - Hat eine Argumenttransformation:
s -> System.out.println("[" + s + "]"). - Hat Kontrollfluss:
n -> n < 0 ? 0 : n. - Ordnet Argumente um oder dupliziert sie:
(a, b) -> b.compareTo(a)(umgekehrter Comparator).
Die Optimierung betrifft nicht wirklich die Laufzeitgeschwindigkeit — beide kompilieren zum selben invokedynamic-Bootstrap. Es geht ums Lesen. Person::name springt einem als "das Feld name" ins Auge, während p -> p.name() das Lesen von drei Tokens erfordert. Wenn die Referenz passt, bevorzugen Sie sie; wenn nicht, verbiegen Sie den Code nicht, um sie hineinzuzwingen.
Eine Falle bei Konstruktorreferenzen: mehrdeutige Überladungen
ClassName::new funktioniert gut, wenn es einen Konstruktor gibt, der zum Ziel-Interface passt. Wenn es mehrere gibt, wählt der Compiler anhand der Parameteranzahl und -typen des Zieltyps. Meistens funktioniert das; gelegentlich nicht, und man muss durch explizite Typisierung der Variable oder durch Rückgriff auf ein Lambda disambiguieren:
// ArrayList has constructors: (), (int), (Collection)
Supplier<ArrayList<String>> a = ArrayList::new; // picks the no-arg
Function<Integer, ArrayList<String>> b = ArrayList::new; // picks the (int) one
Function<List<String>, ArrayList<String>> c = ArrayList::new; // picks the (Collection) one
// var inference can't disambiguate — this would not compile:
// var ambiguous = ArrayList::new;Die Lösung ist, den Zieltyp explizit zu halten, wie bei a, b, c oben.
Ein vollständiges Beispiel: alle vier Formen in einem Programm
Das folgende Programm erstellt und verwendet je eine Methodenreferenz jeder Form, demonstriert, wie String::length (ungebunden) zu einer Function<String, Integer> wird, und zeigt den Konstruktorreferenz-Trick, der stream().toArray(T[]::new) antreibt.
Was man aus der Ausgabe mitnehmen sollte:
- Alle vier Formen kompilieren zu Instanzen gewöhnlicher funktionaler Interfaces —
parseist eineFunction<String, Integer>, egal ob Sies -> Integer.parseInt(s)oderInteger::parseIntgeschrieben haben. Die Kurzform ist rein syntaktisch. - Die ungebundenen
String::lengthundString::toUpperCasehaben beide einen Empfänger als ersten Parameter. Deshalb istString::lengtheineFunction<String, Integer>undString::startsWitheinBiPredicate<String, String>— der Empfänger belegt einen Slot, der explizite Parameter den anderen. - Die Konstruktorreferenz
String[]::newerzeugte eineIntFunction<String[]>— die Form, diestream().toArray(...)erwartet. Konstruktorreferenzen sind die Art, wie man einem Stream sagt "hier ist der Zieltyp." - Der umgekehrte Längen-Comparator konnte nicht als Methodenreferenz geschrieben werden: Empfänger und Parameter tauschen die Plätze, und Methodenreferenzen können Argumente nicht umordnen. Das ist genau die Art von Fall, wo ein Lambda weiterhin die richtige Wahl ist.
Was kommt als Nächstes
Sie können jetzt eine Stream-Pipeline fast vollständig in Methodenreferenzen schreiben und die wenigen Transformationen, die tatsächlich Formgebung benötigen, in kleinen Lambdas leben lassen. Dieser Stil ist der natürliche Einstieg zum Herzstück des Teils: Streams. Das nächste Kapitel, Java Streams Introduction, stellt die Stream<T>-API vor — was sie ist, wie eine Stream-Pipeline aussieht, warum sie lazy ist, warum sie nur einmal verwendet werden kann und wie sie mit den Lambdas, funktionalen Interfaces und Methodenreferenzen zusammenpasst, die Sie gerade gelernt haben.