Sehr verwirrt durch Java 8 Comparator Typ Inferenz

84

Ich habe den Unterschied zwischen Collections.sortund untersucht list.sort, insbesondere in Bezug auf die Verwendung der Comparatorstatischen Methoden und ob Parametertypen in den Lambda-Ausdrücken erforderlich sind. Bevor wir beginnen, weiß ich, dass ich Methodenreferenzen verwenden könnte, z. B. Song::getTitleum meine Probleme zu lösen, aber meine Abfrage hier ist nicht so sehr etwas, das ich beheben möchte, sondern etwas, auf das ich eine Antwort haben möchte, dh warum behandelt der Java-Compiler dies so .

Das sind meine Erkenntnisse. Angenommen, wir haben einen ArrayListTyp Song, bei dem einige Songs hinzugefügt wurden. Es gibt drei Standard-Get-Methoden:

    ArrayList<Song> playlist1 = new ArrayList<Song>();

    //add some new Song objects
    playlist.addSong( new Song("Only Girl (In The World)", 235, "Rhianna") );
    playlist.addSong( new Song("Thinking of Me", 206, "Olly Murs") );
    playlist.addSong( new Song("Raise Your Glass", 202,"P!nk") );

Hier ist ein Aufruf beider Arten von Sortiermethoden, die funktionieren, kein Problem:

Collections.sort(playlist1, 
            Comparator.comparing(p1 -> p1.getTitle()));

playlist1.sort(
            Comparator.comparing(p1 -> p1.getTitle()));

Sobald ich anfange zu verketten thenComparing, passiert Folgendes:

Collections.sort(playlist1,
            Comparator.comparing(p1 -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

playlist1.sort(
        Comparator.comparing(p1 -> p1.getTitle())
        .thenComparing(p1 -> p1.getDuration())
        .thenComparing(p1 -> p1.getArtist())
        );

dh Syntaxfehler, weil es den Typ von p1nicht mehr kennt . Um dies zu beheben, füge ich den Typ Songzum ersten Parameter (zum Vergleichen) hinzu:

Collections.sort(playlist1,
            Comparator.comparing((Song p1) -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

playlist1.sort(
        Comparator.comparing((Song p1) -> p1.getTitle())
        .thenComparing(p1 -> p1.getDuration())
        .thenComparing(p1 -> p1.getArtist())
        );

Jetzt kommt hier der verwirrende Teil. Für p laylist1.sort, dh die Liste, werden alle Kompilierungsfehler für beide folgenden thenComparingAufrufe behoben. Doch für Collections.sort, sie löst es für die erste, aber nicht das letzte. Ich habe getestet, dass mehrere zusätzliche Aufrufe hinzugefügt wurden, thenComparingund es wird immer ein Fehler für den letzten angezeigt, es sei denn, ich habe (Song p1)den Parameter eingegeben.

Nun habe ich dies weiter getestet, indem ich ein erstellt TreeSetund Folgendes verwendet habe Objects.compare:

int x = Objects.compare(t1, t2, 
                Comparator.comparing((Song p1) -> p1.getTitle())
                .thenComparing(p1 -> p1.getDuration())
                .thenComparing(p1 -> p1.getArtist())
                );


    Set<Song> set = new TreeSet<Song>(
            Comparator.comparing((Song p1) -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

Es passiert dasselbe wie in, denn TreeSetes gibt keine Kompilierungsfehler, aber Objects.comparebeim letzten Aufruf thenComparingwird ein Fehler angezeigt.

Kann jemand bitte erklären, warum dies geschieht und warum es überhaupt nicht nötig ist, es zu verwenden, (Song p1)wenn einfach die Vergleichsmethode aufgerufen wird (ohne weitere thenComparingAufrufe).

Eine andere Abfrage zum gleichen Thema ist, wenn ich dies tue TreeSet:

Set<Song> set = new TreeSet<Song>(
            Comparator.comparing(p1 -> p1.getTitle())
            .thenComparing(p1 -> p1.getDuration())
            .thenComparing(p1 -> p1.getArtist())
            );

dh den Typ Songaus dem ersten Lambda-Parameter für den Aufruf der Vergleichsmethode entfernen , es werden Syntaxfehler unter dem Aufruf zum Vergleichen und dem ersten Aufruf zum, thenComparingaber nicht zum letzten Aufruf von angezeigt thenComparing- fast das Gegenteil von dem, was oben geschah! Während für alle die anderen drei Beispiele , dh mit Objects.compare, List.sortund Collections.sortwenn ich entfernen , die erste Songparam Typ es zeigt Syntaxfehler für alle Anrufe.

Vielen Dank im Voraus.

Bearbeitet, um einen Screenshot von Fehlern aufzunehmen, die ich in Eclipse Kepler SR2 erhalten habe. Die ich seitdem gefunden habe, sind Eclipse-spezifisch, da sie beim Kompilieren mit dem JDK8-Java-Compiler in der Befehlszeile OK kompiliert werden.

Sortierfehler in Eclipse

Ruhe
quelle
Es wäre hilfreich, wenn Sie alle Kompilierungsfehlermeldungen, die Sie in all Ihren Tests erhalten, in Ihre Frage aufnehmen.
Eran
1
Um ehrlich zu sein, denke ich, dass es für jemanden am einfachsten ist, die Probleme zu erkennen, indem er den Quellcode selbst ausführt.
Ruhe
Welche Arten von t1und t2im Objects.compareBeispiel? Ich versuche, sie abzuleiten, aber es ist unlösbar, meine Typinferenz über die Typinferenz des Compilers zu legen. :-)
Stuart Marks
1
Welchen Compiler verwenden Sie auch?
Stuart Marks
1
Sie haben hier zwei verschiedene Probleme. Einer der Antwortenden wies darauf hin, dass Sie Methodenreferenzen verwenden könnten, die Sie irgendwie abgebürstet haben. So wie Lambdas sowohl in den Geschmacksrichtungen "explizit typisiert" als auch "implizit typisiert" erhältlich sind, gibt es Methodenreferenzen in den Geschmacksrichtungen "genau" (eine Überladung) und "ungenau" (mehrere Überladungen). Entweder eine genaue Methodenreferenz oder ein explizites Lambda kann verwendet werden, um zusätzliche Tippinformationen bereitzustellen, wenn diese nicht vorhanden sind. (Explizite Zeugen und Abgüsse können ebenfalls verwendet werden, sind jedoch häufig größere Hämmer.)
Brian Goetz

Antworten:

105

Erstens werden alle Beispiele, von denen Sie sagen, dass sie Fehler verursachen, mit der Referenzimplementierung (javac aus JDK 8) gut kompiliert. Sie funktionieren auch in IntelliJ einwandfrei, sodass die angezeigten Fehler möglicherweise Eclipse-spezifisch sind.

Ihre zugrunde liegende Frage scheint zu sein: "Warum funktioniert es nicht mehr, wenn ich anfange zu verketten?" Der Grund hierfür ist, während Lambda - Ausdrücke und generische Methode Anrufungen sind Poly Ausdrücke (ihre Art ist kontextsensitiv) , wenn sie als Methodenparameter angezeigt werden , wenn sie stattdessen als Methode Empfänger Ausdrücke erscheinen, sind sie nicht.

Wenn du sagst

Collections.sort(playlist1, comparing(p1 -> p1.getTitle()));

Es gibt genügend Typinformationen, um sowohl das Typargument comparing()als auch den Argumenttyp zu lösen p1. Der comparing()Aufruf erhält seinen Zieltyp aus der Signatur von Collections.sort, daher ist bekannt, dass er a zurückgeben comparing()muss Comparator<Song>und daher sein p1muss Song.

Aber wenn Sie anfangen zu verketten:

Collections.sort(playlist1,
                 comparing(p1 -> p1.getTitle())
                     .thenComparing(p1 -> p1.getDuration())
                     .thenComparing(p1 -> p1.getArtist()));

Jetzt haben wir ein Problem. Wir wissen, dass der zusammengesetzte Ausdruck comparing(...).thenComparing(...)einen Zieltyp von hat Comparator<Song>, aber da der Empfängerausdruck für die Kette comparing(p -> p.getTitle())ein generischer Methodenaufruf ist und wir seine Typparameter nicht aus den anderen Argumenten ableiten können, haben wir Pech . Da wir den Typ dieses Ausdrucks nicht kennen, wissen wir nicht, dass er eine thenComparingMethode usw. hat.

Es gibt verschiedene Möglichkeiten, dies zu beheben. Alle umfassen das Einfügen weiterer Typinformationen, damit das ursprüngliche Objekt in der Kette ordnungsgemäß eingegeben werden kann. Hier sind sie in grober Reihenfolge abnehmender Begehrlichkeit und zunehmender Eindringlichkeit:

  • Verwenden Sie eine genaue Methodenreferenz (eine ohne Überlastungen), wie z Song::getTitle. Dies gibt dann genügend Typinformationen, um auf die Typvariablen für den comparing()Aufruf zu schließen , und gibt ihm daher einen Typ, und setzt daher die Kette fort.
  • Verwenden Sie ein explizites Lambda (wie in Ihrem Beispiel).
  • Geben Sie einen Typzeugen für den comparing()Anruf an : Comparator.<Song, String>comparing(...).
  • Stellen Sie einen expliziten Zieltyp mit einer Umwandlung bereit, indem Sie den Empfängerausdruck in umwandeln Comparator<Song>.
Brian Goetz
quelle
13
+1 für die tatsächliche Beantwortung des OP "Warum kann der Compiler dies nicht ableiten", anstatt nur Problemumgehungen / Lösungen anzugeben.
Joffrey
Vielen Dank für Ihre Antwort Brian. Ich finde jedoch immer noch etwas unbeantwortet, warum verhält sich List.sort anders als Collections.sort, da für das erstere nur das erste Lambda den Parametertyp enthalten muss, für das letztere jedoch auch das letzte, z. B. wenn ich einen Vergleich habe gefolgt von 5 thenComparing-Aufrufen müsste ich (Song p1) in den Vergleich und in den letzten thenComparing einfügen. Auch in meinem ursprünglichen Beitrag sehen Sie das untere Beispiel des TreeSet, in dem ich alle Parametertypen entferne und der letzte Aufruf von thenComparing in Ordnung ist, die anderen jedoch nicht - dies verhält sich also anders.
Ruhe
3
@ user3780370 Verwenden Sie immer noch den Eclipse-Compiler? Ich habe dieses Verhalten nicht gesehen, wenn ich Ihre Frage richtig verstehe. Können Sie (a) es mit Javac aus JDK 8 versuchen und (b) den Code posten, wenn es immer noch fehlschlägt?
Brian Goetz
@BrianGoetz Danke für diesen Vorschlag. Ich habe es gerade mit Javac im Befehlsfenster kompiliert und es wird wie gesagt kompiliert. Es scheint ein Eclipse-Problem zu sein. Ich habe noch nicht auf Eclipse Luna aktualisiert, das speziell für JDK8 entwickelt wurde. Hoffentlich kann es darin behoben werden. Ich habe tatsächlich einen Screenshot, der Ihnen zeigt, was in Eclipse passiert ist, weiß aber nicht, wie ich hier posten soll.
Ruhe
2
Ich denke du meinst Comparator.<Song, String>comparing(...).
Shmosel
23

Das Problem ist die Typinferenz. Ohne (Song s)dem ersten Vergleich ein hinzuzufügen , comparator.comparingkennt der Typ der Eingabe nicht, daher wird standardmäßig Object verwendet.

Sie können dieses Problem auf eine von drei Arten beheben:

  1. Verwenden Sie die neue Java 8-Methodenreferenzsyntax

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  2. Ziehen Sie jeden Vergleichsschritt in eine lokale Referenz

      Comparator<Song> byName = (s1, s2) -> s1.getArtist().compareTo(s2.getArtist());
    
      Comparator<Song> byDuration = (s1, s2) -> Integer.compare(s1.getDuration(), s2.getDuration());
    
        Collections.sort(playlist,
                byName
                .thenComparing(byDuration)
                );
    

    BEARBEITEN

  3. Erzwingen des vom Komparator zurückgegebenen Typs (beachten Sie, dass Sie sowohl den Eingabetyp als auch den Vergleichsschlüsseltyp benötigen)

    sort(
      Comparator.<Song, String>comparing((s) -> s.getTitle())
                .thenComparing(p1 -> p1.getDuration())
                .thenComparing(p1 -> p1.getArtist())
                );
    

Ich denke, der "letzte" thenComparingSyntaxfehler führt Sie in die Irre. Es ist eigentlich ein Typproblem mit der gesamten Kette, es ist nur der Compiler, der nur das Ende der Kette als Syntaxfehler markiert, da dann der endgültige Rückgabetyp vermutlich nicht übereinstimmt.

Ich bin mir nicht sicher, warum Listein besserer Inferenzjob ausgeführt wird, als Collectionda er denselben Erfassungstyp ausführen sollte, aber anscheinend nicht.

dkatzel
quelle
Warum weiß es es für die, ArrayListaber nicht für die CollectionsLösung (wenn der erste Aufruf in der Kette einen SongParameter hat)?
Sotirios Delimanolis
4
Vielen Dank für Ihre Antwort. Wenn Sie jedoch meinen Beitrag lesen, werden Sie sehen, dass ich sagte: "Bevor wir beginnen, weiß ich, dass ich Methodenreferenzen verwenden kann, z. B. Song :: getTitle, um meine Probleme zu lösen, aber meine Abfrage hier ist nicht so sehr etwas, das ich reparieren möchte, aber etwas, auf das ich eine Antwort haben möchte, dh warum behandelt der Java-Compiler es so. "
Ruhe
Ich möchte eine Antwort darauf, warum sich der Compiler so verhält, wenn ich Lambda-Ausdrücke verwende. Es akzeptiert das Vergleichen (s -> s.getArtist ()), aber wenn ich dann .thenComparing (s -> s.getDuration ()) verkette, gibt es mir Syntaxfehler für beide Aufrufe, wenn ich dann einen expliziten Typ einfüge Der Vergleichsaufruf, z. B. Vergleich ((Song s) -> s.getArtist ()), behebt dieses Problem und löst für List.sort und TreeSet auch alle weiteren Kompilierungsfehler, ohne dass zusätzliche Parametertypen hinzugefügt werden müssen, z Die Collections.sort & Objects.Vergleichen Sie Beispiele, die der letzte Vergleich noch fehlschlägt
Tranquility
1

Eine andere Möglichkeit, mit diesem Fehler bei der Kompilierung umzugehen:

Setzen Sie die Variable Ihrer ersten Vergleichsfunktion explizit um und los geht's. Ich habe die Liste der org.bson.Documents-Objekte sortiert. Bitte schauen Sie sich den Beispielcode an

Comparator<Document> comparator = Comparator.comparing((Document hist) -> (String) hist.get("orderLineStatus"), reverseOrder())
                       .thenComparing(hist -> (Date) hist.get("promisedShipDate"))
                       .thenComparing(hist -> (Date) hist.get("lastShipDate"));
list = list.stream().sorted(comparator).collect(Collectors.toList());
Rajni Gangwar
quelle
0

playlist1.sort(...) Erstellt eine Song-Grenze für die Typvariable E aus der Deklaration von Playlist1, die den Komparator "kräuselt".

In Collections.sort(...)gibt es keine solche Grenze, und die Schlussfolgerung aus dem Typ des ersten Komparators reicht nicht aus, damit der Compiler auf den Rest schließen kann.

Ich denke, Sie würden "korrektes" Verhalten erhalten Collections.<Song>sort(...), aber Sie müssen kein Java 8 installieren, um es für Sie zu testen.

Amalloy
quelle
Hallo, ja, Sie haben Recht, wenn Sie Sammlungen hinzufügen. <Song> beseitigt den Fehler für den letzten Vergleichsaufruf
Tranquility