.Min () und .max () des Java 8-Streams: Warum wird dies kompiliert?

215

Hinweis: Diese Frage stammt von einem toten Link, der eine vorherige SO-Frage war, aber hier geht ...

Siehe diesen Code ( Hinweis: Ich weiß, dass dieser Code nicht "funktioniert" und Integer::compareverwendet werden sollte - ich habe ihn gerade aus der verknüpften Frage extrahiert ):

final ArrayList <Integer> list 
    = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

System.out.println(list.stream().max(Integer::max).get());
System.out.println(list.stream().min(Integer::min).get());

Nach dem Javadoc von .min()und .max()sollte das Argument von beiden a sein Comparator. Hier beziehen sich die Methodenreferenzen jedoch auf statische Methoden der IntegerKlasse.

Warum wird das überhaupt kompiliert?

fge
quelle
6
Beachten Sie, dass es nicht richtig funktioniert, sondern Integer::compareanstelle von Integer::maxund verwendet werden sollte Integer::min.
Christoffer Hammarström
@ ChristofferHammarström das weiß ich; Beachten Sie, wie ich vor dem Code-Auszug sagte: "Ich weiß, es ist absurd"
fge
3
Ich habe nicht versucht, dich zu korrigieren, ich sage es den Leuten im Allgemeinen. Sie haben es so klingen lassen, als ob Sie dachten, der absurde Teil sei, dass Methoden Integerkeine Methoden von sind Comparator.
Christoffer Hammarström

Antworten:

242

Lassen Sie mich erklären, was hier passiert, denn es ist nicht offensichtlich!

Erstens Stream.max()nimmt eine Instanz Comparatorso , dass Gegenstände in dem Strom können gegeneinander verglichen werden , das Minimum oder Maximum zu finden, die in einem gewissen optimalen Reihenfolge , dass Sie nicht zu viel Sorgen zu machen brauchen.

Die Frage ist also natürlich, warum Integer::maxakzeptiert wird? Immerhin ist es kein Komparator!

Die Antwort liegt in der Art und Weise, wie die neue Lambda-Funktionalität in Java 8 funktioniert. Sie basiert auf einem Konzept, das informell als "Single Abstract Method" -Schnittstellen oder "SAM" -Schnittstellen bezeichnet wird. Die Idee ist, dass jede Schnittstelle mit einer abstrakten Methode automatisch von jedem Lambda oder jeder Methodenreferenz implementiert werden kann, deren Methodensignatur mit der einen Methode auf der Schnittstelle übereinstimmt. Untersuchen Sie also die ComparatorSchnittstelle (einfache Version):

public Comparator<T> {
    T compare(T o1, T o2);
}

Wenn eine Methode nach a sucht Comparator<Integer>, sucht sie im Wesentlichen nach dieser Signatur:

int xxx(Integer o1, Integer o2);

Ich verwende "xxx", weil der Methodenname nicht für Übereinstimmungszwecke verwendet wird .

Daher sind beide Integer.min(int a, int b)und Integer.max(int a, int b)nahe genug, dass Autoboxing dies als Comparator<Integer>in einem Methodenkontext erscheinen lässt.

David M. Lloyd
quelle
28
oder alternativ : list.stream().mapToInt(i -> i).max().get().
Assylias
13
@assylias Sie möchten .getAsInt()stattdessen verwenden get(), da Sie es mit einem zu tun haben OptionalInt.
Skiwi
... wenn wir nur versuchen, einen benutzerdefinierten Komparator für eine max()Funktion bereitzustellen !
Manu343726
Es ist erwähnenswert, dass diese "SAM-Schnittstelle" tatsächlich als "funktionale Schnittstelle" bezeichnet wird und dass Comparatorwir anhand der Dokumentation sehen können, dass sie mit der Anmerkung versehen ist @FunctionalInterface. Dieser Dekorateur ist die Magie, die es ermöglicht Integer::maxund Integer::minin eine umgewandelt werden kann Comparator.
Chris Kerekes
2
@ChrisKerekes Der Dekorateur @FunctionalInterfacedient hauptsächlich zu Dokumentationszwecken, da der Compiler dies problemlos mit jeder Schnittstelle mit einer einzigen abstrakten Methode tun kann.
Errantlinguist
117

Comparatorist eine funktionale Schnittstelle und Integer::maxentspricht dieser Schnittstelle (nachdem Autoboxing / Unboxing berücksichtigt wurde). Es nimmt zwei intWerte an und gibt ein zurück int- genau wie Sie es erwarten würden Comparator<Integer>(erneut Schielen, um den Unterschied zwischen Ganzzahl und Int zu ignorieren).

Ich würde jedoch nicht erwarten, dass es das Richtige tut, da Integer.maxdies nicht der Semantik von entspricht Comparator.compare. Und tatsächlich funktioniert es im Allgemeinen nicht wirklich. Nehmen Sie zum Beispiel eine kleine Änderung vor:

for (int i = 1; i <= 20; i++)
    list.add(-i);

... und jetzt ist der maxWert -20 und der minWert -1.

Stattdessen sollten beide Aufrufe Folgendes verwenden Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());
Jon Skeet
quelle
1
Ich kenne die funktionalen Schnittstellen; und ich weiß, warum es die falschen Ergebnisse gibt. Ich frage mich nur, wie um
alles
6
@fge: Nun, war es das Unboxing, über das du unklar warst? (Ich habe nicht genau diesen Teil untersucht.) A Comparator<Integer>hätte int compare(Integer, Integer)... es ist nicht umwerfend, dass Java eine Methodenreferenz zulässt int max(int, int), um darauf zu konvertieren ...
Jon Skeet
7
@fge: Soll der Compiler die Semantik von kennen Integer::max? Aus seiner Sicht haben Sie eine Funktion übergeben, die ihrer Spezifikation entspricht. Das ist alles, was sie wirklich tun kann.
Mark Peters
6
@fge: Insbesondere wenn Sie einen Teil der Vorgänge verstehen , sich aber für einen bestimmten Aspekt interessieren, sollten Sie dies in der Frage klarstellen, um zu vermeiden, dass Personen ihre Zeit damit verschwenden, die bereits bekannten Teile zu erklären.
Jon Skeet
20
Ich denke, das zugrunde liegende Problem ist die Typensignatur von Comparator.compare. Es sollte ein enumvon zurückgeben {LessThan, GreaterThan, Equal}, kein int. Auf diese Weise würde die Funktionsschnittstelle nicht wirklich übereinstimmen und Sie würden einen Kompilierungsfehler erhalten. IOW: Die Typensignatur von Comparator.compareerfasst die Semantik dessen, was es bedeutet, zwei Objekte zu vergleichen, nicht angemessen, und daher haben andere Schnittstellen, die absolut nichts mit dem versehentlichen Vergleichen von Objekten zu tun haben, dieselbe Typensignatur.
Jörg W Mittag
19

Dies funktioniert, weil Integer::mineine Implementierung der Comparator<Integer>Schnittstelle aufgelöst wird.

Die Methodenreferenz von Integer::minAuflösen Integer.min(int a, int b), Auflösen IntBinaryOperatorund vermutlich Autoboxen tritt irgendwo auf, wodurch es zu einem wird BinaryOperator<Integer>.

Und die min()resp- max()Methoden der Stream<Integer>fordern die Comparator<Integer>zu implementierende Schnittstelle auf.
Dies wird nun auf die einzelne Methode aufgelöst Integer compareTo(Integer o1, Integer o2). Welches ist vom Typ BinaryOperator<Integer>.

Und so ist die Magie passiert, da beide Methoden a sind BinaryOperator<Integer>.

Skiwi
quelle
Es ist nicht ganz richtig zu sagen, dass Integer::minimplementiert Comparable. Es ist kein Typ, der irgendetwas implementieren kann. Aber es wird zu einem Objekt ausgewertet, das implementiert Comparable.
Lii
1
@ Lii Danke, ich habe es gerade behoben.
Skiwi
Comparator<Integer>ist eine Single-Abstract-Method-Schnittstelle (auch als "funktional" bezeichnet) und Integer::minerfüllt ihren Vertrag, sodass das Lambda so interpretiert werden kann. Ich weiß nicht, wie BinaryOperator hier ins Spiel kommt (oder auch IntBinaryOperator) - es gibt keine Subtypisierungsbeziehung zwischen diesem und Comparator.
Paŭlo Ebermann
2

Abgesehen von den Informationen von David M. Lloyd könnte man hinzufügen, dass der Mechanismus, der dies ermöglicht, als Zieltypisierung bezeichnet wird .

Die Idee ist, dass der Typ, den der Compiler einem Lambda-Ausdruck oder einer Methodenreferenz zuweist, nicht nur vom Ausdruck selbst abhängt, sondern auch davon, wo er verwendet wird.

Das Ziel eines Ausdrucks ist die Variable, der sein Ergebnis zugewiesen ist, oder der Parameter, an den sein Ergebnis übergeben wird.

Lambda-Ausdrücken und Methodenreferenzen wird ein Typ zugewiesen, der dem Typ ihres Ziels entspricht, sofern ein solcher Typ gefunden werden kann.

Siehe die Type Inference Abschnitt in den Java - Tutorial für weitere Informationen.

Lii
quelle
1

Ich hatte einen Fehler mit einem Array, das das Maximum und das Minimum erhielt. Meine Lösung war also:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
JPRLCol
quelle