W3docs

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 reference

Die 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:

LambdaMethodenreferenz
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.

java— editable, runs on the server

Was man aus der Ausgabe mitnehmen sollte:

  • Alle vier Formen kompilieren zu Instanzen gewöhnlicher funktionaler Interfaces — parse ist eine Function<String, Integer>, egal ob Sie s -> Integer.parseInt(s) oder Integer::parseInt geschrieben haben. Die Kurzform ist rein syntaktisch.
  • Die ungebundenen String::length und String::toUpperCase haben beide einen Empfänger als ersten Parameter. Deshalb ist String::length eine Function<String, Integer> und String::startsWith ein BiPredicate<String, String> — der Empfänger belegt einen Slot, der explizite Parameter den anderen.
  • Die Konstruktorreferenz String[]::new erzeugte eine IntFunction<String[]> — die Form, die stream().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.

Übung

Übung
`String::length` ist welche Art von Methodenreferenz, und welches funktionale Interface passt dazu?
`String::length` ist welche Art von Methodenreferenz, und welches funktionale Interface passt dazu?
Was this page helpful?