Java 8 Lambdas, Function.identity () oder t-> t

240

Ich habe eine Frage zur Verwendung der Function.identity()Methode.

Stellen Sie sich den folgenden Code vor:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

Gibt es einen Grund, warum Sie Function.identity()anstelle von str->str(oder umgekehrt) verwenden sollten. Ich denke, dass die zweite Option besser lesbar ist (Geschmackssache natürlich). Aber gibt es einen "echten" Grund, warum man bevorzugt werden sollte?

Przemysław Głębocki
quelle
6
Letztendlich macht das keinen Unterschied.
fge
50
Beides ist in Ordnung. Gehen Sie mit dem, was Sie für lesbarer halten. (Mach dir keine Sorgen, sei glücklich.)
Brian Goetz
3
Ich würde es t -> teinfach vorziehen , weil es prägnanter ist.
David Conrad
3
Etwas unabhängige Frage, aber weiß jemand, warum die Sprachdesigner identity () eine Instanz von Function zurückgeben, anstatt einen Parameter vom Typ T zu haben und ihn zurückzugeben, damit die Methode mit Methodenreferenzen verwendet werden kann?
Kirill Rakhman
Ich würde argumentieren, dass es sinnvoll ist, mit dem Wort "Identität" vertraut zu sein, da es in anderen Bereichen der funktionalen Programmierung eine wichtige Bedeutung hat.
Orbfish

Antworten:

311

Ab der aktuellen JRE-Implementierung Function.identity()wird immer dieselbe Instanz zurückgegeben, während bei jedem Auftreten von identifier -> identifiernicht nur eine eigene Instanz erstellt wird, sondern sogar eine eigene Implementierungsklasse vorhanden ist. Weitere Details finden Sie hier .

Der Grund dafür ist, dass der Compiler eine synthetische Methode generiert, die den Trivialkörper dieses Lambda-Ausdrucks enthält (im Fall von x->xäquivalent zu return identifier;) und die Laufzeit anweist, eine Implementierung der funktionalen Schnittstelle zu erstellen, die diese Methode aufruft. Die Laufzeit sieht also nur unterschiedliche Zielmethoden und die aktuelle Implementierung analysiert die Methoden nicht, um herauszufinden, ob bestimmte Methoden gleichwertig sind.

Wenn Sie also Function.identity()anstelle von x -> xverwenden, sparen Sie möglicherweise Speicherplatz, aber das sollte Ihre Entscheidung nicht beeinflussen, wenn Sie der Meinung sind, dass dies x -> xbesser lesbar ist als Function.identity().

Sie können auch berücksichtigen, dass die synthetische Methode beim Kompilieren mit aktivierten Debug-Informationen ein Zeilen-Debug-Attribut aufweist, das auf die Quellcode-Zeile (n) verweist, in denen sich der Lambda-Ausdruck befindet. Daher haben Sie die Möglichkeit, die Quelle einer bestimmten FunctionInstanz beim Debuggen zu finden . Wenn Sie dagegen auf die Instanz stoßen, die Function.identity()beim Debuggen einer Operation zurückgegeben wurde, wissen Sie nicht, wer diese Methode aufgerufen und die Instanz an die Operation übergeben hat.

Holger
quelle
5
Gute Antwort. Ich habe einige Zweifel am Debuggen. Wie kann es nützlich sein? Es ist sehr unwahrscheinlich, dass der Exception-Stack-Trace einen x -> xFrame enthält. Schlagen Sie vor, den Haltepunkt auf dieses Lambda zu setzen? Normalerweise ist es nicht so einfach, den Haltepunkt in das Lambda mit einem Ausdruck zu setzen (zumindest in Eclipse) ...
Tagir Valeev
14
@Tagir Valeev: Sie können Code debuggen, der eine beliebige Funktion empfängt, und in die Apply-Methode dieser Funktion eintreten. Dann könnten Sie am Quellcode eines Lambda-Ausdrucks landen. Im Fall eines expliziten Lambda-Ausdrucks wissen Sie, woher die Funktion kommt, und haben die Möglichkeit zu erkennen, an welcher Stelle die Entscheidung zum Bestehen einer Identitätsfunktion getroffen wurde. Bei Verwendung gehen Function.identity()diese Informationen verloren. Dann kann die Aufrufkette in einfachen Fällen hilfreich sein, aber denken Sie beispielsweise an eine Multithread-Auswertung, bei der sich der ursprüngliche Initiator nicht in der Stapelverfolgung befindet…
Holger,
2
Interessant in diesem Zusammenhang: blog.codefx.org/java/instances-non-capturing-lambdas
Wim Deblauwe
13
@Wim Deblauwe: Interessant, aber ich würde es immer umgekehrt sehen: Wenn eine Factory-Methode in ihrer Dokumentation nicht explizit angibt, dass sie bei jedem Aufruf eine neue Instanz zurückgibt, können Sie nicht davon ausgehen, dass dies der Fall ist. Es sollte also nicht überraschen, wenn dies nicht der Fall ist. Dies ist schließlich ein wichtiger Grund für die Verwendung von Factory-Methoden anstelle von new. new Foo(…)Garantien, um eine neue Instanz des genauen Typs zu erstellen Foo, während Foo.getInstance(…)möglicherweise eine vorhandene Instanz von (einem Subtyp von) zurückgegeben wird Foo
Holger
93

In Ihrem Beispiel gibt es keinen großen Unterschied zwischen str -> strund Function.identity()da es intern einfach ist t->t.

Aber manchmal können wir nicht verwenden, Function.identityweil wir a nicht verwenden können Function. Schauen Sie hier:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

Dies wird gut kompiliert

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

aber wenn Sie versuchen zu kompilieren

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

Sie erhalten einen Kompilierungsfehler, da mapToInterwartet wird ToIntFunction, was nicht damit zusammenhängt Function. Hat ToIntFunctionauch keine identity()Methode.

Pshemo
quelle
3
Siehe stackoverflow.com/q/38034982/14731 für ein anderes Beispiel , bei dem Ersetzen i -> imit Function.identity()in einem Compiler - Fehlern führen wird.
Gili
19
Ich bevorzuge mapToInt(Integer::intValue).
Shmosel
4
@shmosel das ist OK, aber es ist erwähnenswert, dass beide Lösungen ähnlich funktionieren, da mapToInt(i -> i)es eine Vereinfachung von ist mapToInt( (Integer i) -> i.intValue()). Verwenden Sie die Version, die Sie für klarer halten. Für mich mapToInt(i -> i)zeigt dies besser die Absichten dieses Codes.
Pshemo
1
Ich denke, dass die Verwendung von Methodenreferenzen Leistungsvorteile haben kann, aber es ist meistens nur eine persönliche Präferenz. Ich finde es aussagekräftiger, weil es i -> iwie eine Identitätsfunktion aussieht, die es in diesem Fall nicht ist.
Shmosel
@shmosel Ich kann nicht viel über Leistungsunterschiede sagen, also hast du vielleicht Recht. Aber wenn Leistung kein Problem ist, werde ich bleiben, i -> ida mein Ziel darin besteht, Integer int zuzuordnen (was mapToIntsehr gut nahelegt), die intValue()Methode nicht explizit aufzurufen . Wie dieses Mapping erreicht wird, ist nicht wirklich so wichtig. Lassen Sie uns also einfach zustimmen, nicht zuzustimmen, aber danke, dass Sie auf mögliche Leistungsunterschiede hingewiesen haben, muss ich mir das eines Tages genauer ansehen.
Pshemo
44

Aus der JDK-Quelle :

static <T> Function<T, T> identity() {
    return t -> t;
}

Also nein, solange es syntaktisch korrekt ist.

JasonN
quelle
8
Ich frage mich, ob dies die obige Antwort in Bezug auf ein Lambda, das ein Objekt erstellt, ungültig macht - oder ob dies eine bestimmte Implementierung ist.
Orbfish
28
@orbfish: das stimmt perfekt. Jedes Vorkommen t->tim Quellcode kann ein Objekt erzeugen und die Implementierung von Function.identity()ist ein Vorkommen. Alle aufgerufenen Aufrufseiten identity()teilen also dieses eine Objekt, während alle Sites, die explizit den Lambda-Ausdruck verwenden t->t, ihr eigenes Objekt erstellen. Die Methode Function.identity()ist in keiner Weise speziell. Wenn Sie eine Factory-Methode erstellen, die einen häufig verwendeten Lambda-Ausdruck kapselt, und diese Methode aufrufen, anstatt den Lambda-Ausdruck zu wiederholen, können Sie angesichts der aktuellen Implementierung Speicherplatz sparen .
Holger
Ich vermute, das liegt daran, dass der Compiler die Erstellung eines neuen t->tObjekts bei jedem Aufruf der Methode optimiert und bei jedem Aufruf der Methode dasselbe wiederverwertet.
Daniel Gray
1
@ DanielGray Die Entscheidung wird zur Laufzeit getroffen. Der Compiler fügt einen invokedynamicBefehl ein, der bei seiner ersten Ausführung durch Ausführen einer sogenannten Bootstrap-Methode verknüpft wird, die sich bei Lambda-Ausdrücken in der befindet LambdaMetafactory. Diese Implementierung beschließt, ein Handle an einen Konstruktor, eine Factory-Methode oder einen Code zurückzugeben, der immer dasselbe Objekt zurückgibt. Möglicherweise wird auch entschieden, einen Link zu einem bereits vorhandenen Handle zurückzugeben (was derzeit nicht der Fall ist).
Holger
@Holger Sind Sie sicher, dass dieser Aufruf zur Identität nicht eingebunden und dann möglicherweise monomorphisiert (und erneut eingefügt) wird?
JasonN