it-swarm.com.de

Sehr verwirrt durch Java 8 Comparator-Inferenz

Ich habe den Unterschied zwischen Collections.sort und list.sort untersucht, insbesondere hinsichtlich der Verwendung der statischen Comparator-Methoden und ob Param-Typen in den Lambda-Ausdrücken erforderlich sind. Bevor wir beginnen, kann ich Methodenreferenzen verwenden, z. Song::getTitle, um meine Probleme zu überwinden, aber meine Abfrage hier ist nicht so sehr etwas, das ich reparieren möchte, aber etwas, auf das ich eine Antwort möchte, d. h., warum der Java-Compiler dies so behandelt. 

Das ist meine Feststellung. Angenommen, wir haben eine ArrayList vom Typ Song, mit einigen Liedern gibt es 3 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 an beide 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, thenComparing zu verketten, geschieht 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())
        );

syntaxfehler, da der Typ von p1 nicht mehr bekannt ist. Um dies zu korrigieren, füge ich dem ersten Parameter (vom Vergleich) den Typ Song 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 der CONFUSING-Teil. Bei playlist1.sort, d. H. Der Liste, werden dadurch alle Kompilierungsfehler für die beiden folgenden thenComparing-Aufrufe behoben. Für Collections.sort löst es jedoch die erste, nicht aber die letzte. Ich habe getestet, dass einige zusätzliche Aufrufe für thenComparing hinzugefügt wurden, und es wird immer ein Fehler für den letzten angezeigt, es sei denn, ich setze (Song p1) für den Parameter ein.

Nun testete ich dies weiter, indem ich eine TreeSet erstellte und Objects.compare verwendete:

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())
            );

Dasselbe passiert wie in, für TreeSet gibt es keine Kompilierungsfehler, aber für Objects.compare zeigt der letzte Aufruf von thenComparing einen Fehler.

Kann mir bitte jemand erklären, warum dies geschieht und warum es nicht nötig ist, (Song p1) zu verwenden, wenn Sie einfach die vergleichende Methode aufrufen (ohne weitere thenComparing-Aufrufe).

Eine andere Frage zum selben Thema ist, wenn ich dies mit der Variablen TreeSet mache:

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

entfernen Sie den Typ Song aus dem ersten Lambda-Parameter für den Vergleichsmethodenaufruf. Er zeigt Syntaxfehler unter dem Aufruf von Compare und den ersten Aufruf von thenComparing, jedoch nicht den endgültigen Aufruf von thenComparing - fast das Gegenteil von dem, was oben passiert ist! Bei allen anderen 3 Beispielen, d. H. Mit Objects.compare, List.sort und Collections.sort, wenn ich den ersten Song param-Typ entferne, werden für alle Aufrufe Syntaxfehler angezeigt.

Vielen Dank im Voraus.

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

Sort errors in Eclipse

65
Tranquility

Zunächst kompilieren alle Beispiele, die Sie zu Fehlern führen, gut mit der Referenzimplementierung (javac aus JDK 8.). Sie funktionieren auch in IntelliJ einwandfrei. Daher ist es möglich, dass die Fehler Eclipse-spezifisch sind. 

Ihre zugrunde liegende Frage scheint zu sein: "Warum hört es auf zu funktionieren, wenn ich anfange zu ketten" Der Grund ist, während Lambda-Ausdrücke und generische Methodenaufrufe Poly-Ausdrücke sind (deren Typ kontextsensitiv ist), wenn sie als Methodenparameter erscheinen, wenn sie stattdessen als Methodenempfängerausdrücke erscheinen, sind sie dies nicht. 

Wenn du sagst

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

es gibt genügend Typinformationen, um sowohl für das Typargument von comparing() als auch für den Argumenttyp p1 aufzulösen. Der Aufruf von comparing() erhält seinen Zieltyp von der Signatur von Collections.sort. Daher ist bekannt, dass comparing() einen Comparator<Song> zurückgeben muss und daher p1Song sein muss. 

Aber wenn Sie anfangen zu ketten:

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(...) den Zieltyp Comparator<Song> hat. Da der Empfängerausdruck comparing(p -> p.getTitle()) für die Kette jedoch ein generischer Methodenaufruf ist, können wir seine Typparameter nicht aus den anderen Argumenten ableiten kein Glück. Da wir den Typ dieses Ausdrucks nicht kennen, wissen wir nicht, dass er eine thenComparing-Methode hat usw. 

Es gibt mehrere Möglichkeiten, das Problem zu beheben. Dazu gehört das Einfügen weiterer Typinformationen, sodass das ursprüngliche Objekt in der Kette richtig eingegeben werden kann. Hier sind sie in groben Reihenfolge abnehmender Attraktivität und zunehmender Intrusivität:

  • Verwenden Sie eine genaue Methodenreferenz (eine ohne Überladungen) wie Song::getTitle. Dies gibt dann genügend Typinformationen an, um die Typvariablen für den Aufruf von comparing() abzuleiten und ihm daher einen Typ zu geben und die Kette fortzusetzen.
  • Verwenden Sie ein explizites Lambda (wie in Ihrem Beispiel).
  • Geben Sie einen Typ-Zeugen für den Aufruf comparing() an: Comparator.<Song, String>comparing(...)
  • Geben Sie einen expliziten Zieltyp mit einer Umwandlung an, indem Sie den Empfängerausdruck in Comparator<Song> umwandeln. 
79
Brian Goetz

Das Problem ist die Typinferenz. Wenn Sie dem ersten Vergleich keinen (Song s) hinzufügen, kennt comparator.comparing den Typ der Eingabe nicht. Daher wird standardmäßig Object verwendet.

Sie können dieses Problem auf 1 von 3 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)
                );
    

    EDIT

  3. Erzwingen des vom Comparator zurückgegebenen Typs (Hinweis: Sie benötigen sowohl den Eingabetyp als auch den Vergleichsschlüsseltyp).

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

Ich denke, der "letzte" thenComparing-Syntaxfehler 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, weil dann der endgültige Rückgabetyp nicht stimmt, denke ich.

Ich bin nicht sicher, warum List einen besseren Inferencing-Job als Collection ausführt, da er denselben Capture-Typ ausführen sollte, aber anscheinend nicht.

19
dkatzel

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

Wende die Variable der ersten Vergleichsfunktion explizit an und dann weiter. Ich habe die Liste des org.bson.Documents-Objekts 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());
0
Rajni Gangwar

playlist1.sort(...) erstellt für die Typvariable E eine Begrenzung des Songs aus der Deklaration von playlist1, die sich zum Vergleicher "kräuselt". 

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

Ich denke, Sie würden von Collections.<Song>sort(...) ein "korrektes" Verhalten erhalten, haben aber keine Java 8-Installation, um es für Sie zu testen.

0
amalloy