Bardzo zdezorientowany przez wnioskowanie o typie komparatora Java 8

84

Przyglądałem się różnicy między Collections.sorti list.sort, w szczególności w odniesieniu do używania Comparatormetod statycznych i tego, czy typy parametrów są wymagane w wyrażeniach lambda. Zanim zaczniemy, wiem, że mógłbym użyć referencji do metod, np. Song::getTitleAby przezwyciężyć swoje problemy, ale moje zapytanie tutaj nie jest czymś, co chcę naprawić, ale czymś, na co chcę odpowiedzi, tj. Dlaczego kompilator Java obsługuje to w ten sposób .

Oto moje odkrycie. Załóżmy, że mamy ArrayListtyp Song, z dodanymi utworami, istnieją 3 standardowe metody pobierania:

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

Oto wywołanie obu typów metod sortowania, które działają, nie ma problemu:

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

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

Gdy tylko zacznę łączyć thenComparing, dzieje się co następuje:

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

tj. błędy składniowe, ponieważ nie zna już typu p1. Aby to naprawić, dodaję typ Songdo pierwszego parametru (porównania):

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

Teraz czas na część Mylące. Dla p laylist1.sort, czyli Listy, rozwiązuje to wszystkie błędy kompilacji dla obu poniższych thenComparingwywołań. Jednak Collections.sortrozwiązuje to dla pierwszego, ale nie ostatniego. Testowałem dodałem kilka dodatkowych wywołań do thenComparingi zawsze pokazuje błąd dla ostatniego, chyba że wstawię (Song p1)parametr.

Teraz poszedłem dalej przetestować to, tworząc TreeSeti używając 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())
            );

To samo dzieje się w przypadku TreeSet, nie ma błędów kompilacji, ale Objects.compareostatnie wywołanie thenComparingpokazuje błąd.

Czy ktoś może wyjaśnić, dlaczego tak się dzieje, a także dlaczego w ogóle nie ma potrzeby używania, (Song p1)gdy po prostu wywołuje metodę porównywania (bez dalszych thenComparingwywołań).

Jeszcze jedno zapytanie na ten sam temat dotyczy tego TreeSet:

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

tzn. usuń typ Songz pierwszego parametru lambda dla wywołania metody porównującej, pokaże błędy składniowe pod wywołaniem porównania i pierwsze wywołanie, thenComparingale nie ostatnie wywołanie thenComparing- prawie odwrotność tego, co działo się powyżej! Podczas gdy dla wszystkich pozostałych 3 przykładów, tj. Z Objects.compare, List.sorti Collections.sortkiedy usuwam ten pierwszy Songtyp parametru, pokazuje on błędy składniowe dla wszystkich wywołań.

Z góry bardzo dziękuję.

Edytowano tak, aby zawierał zrzut ekranu błędów, które otrzymałem w Eclipse Kepler SR2, które teraz znalazłem, są specyficzne dla Eclipse, ponieważ po skompilowaniu za pomocą kompilatora JDK8 java w wierszu poleceń kompiluje się OK.

Sortuj błędy w Eclipse

Spokój
źródło
Byłoby pomocne, gdybyś zawarł w swoim pytaniu wszystkie komunikaty o błędach kompilacji, które otrzymujesz we wszystkich testach.
Eran
1
szczerze mówiąc, myślę, że najłatwiej byłoby komuś sprawdzić, na czym polega problem, uruchamiając samodzielnie kod źródłowy.
Tranquility
Jakie są rodzaje t1i t2na Objects.compareprzykład? Próbuję je wywnioskować, ale nakładanie mojego wnioskowania o typ na wnioskowanie o typie kompilatora jest trudne. :-)
Stuart Marks
1
Z jakiego kompilatora korzystasz?
Stuart Marks
1
Masz tutaj dwa oddzielne problemy. Jedna z osób odpowiadających wskazała, że ​​można użyć odwołań do metod, które w pewnym sensie zostały usunięte. Podobnie jak wyrażenia lambda występują zarówno w smakach „jawnie wpisanych”, jak i „wpisanych niejawnie”, odwołania do metod mają charakter „dokładny” (jedno przeciążenie) i „niedokładny” (wielokrotne przeciążenie). Jeśli nie ma, można użyć dokładnej referencji metody lub jawnej lambda, aby podać dodatkowe informacje dotyczące typowania. (Można również użyć jawnych świadków i odlewów, ale często są to większe młoty.)
Brian Goetz

Odpowiedzi:

105

Po pierwsze, wszystkie podane przykłady powodują błędy kompilują się dobrze z implementacją referencyjną (javac z JDK 8.) Działają również dobrze w IntelliJ, więc jest całkiem możliwe, że błędy, które widzisz, są specyficzne dla Eclipse.

Wydaje się, że Twoje podstawowe pytanie brzmi: „dlaczego przestaje działać, kiedy zaczynam łączyć”. Przyczyną jest to, że chociaż wyrażenia lambda i wywołania metod ogólnych są wyrażeniami poli (ich typ jest zależny od kontekstu), gdy pojawiają się jako parametry metody, to gdy pojawiają się jako wyrażenia odbiorcze metody, tak nie jest.

Kiedy powiesz

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

jest wystarczająco dużo informacji o typie do rozwiązania zarówno dla argumentu typu, jak comparing()i dla typu argumentu p1. comparing()Rozmowa staje się jego typ docelowy z podpisem Collections.sort, więc wiadomo, comparing()musi zwrócić Comparator<Song>, a zatem p1musi być Song.

Ale kiedy zaczniesz łączyć:

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

teraz mamy problem. Wiemy, że wyrażenie złożone comparing(...).thenComparing(...)ma typ docelowy Comparator<Song>, ale ponieważ wyrażenie odbierające dla łańcucha comparing(p -> p.getTitle())jest ogólnym wywołaniem metody i nie możemy wywnioskować jego parametrów typu z innych argumentów, nie mamy szczęścia . Ponieważ nie znamy typu tego wyrażenia, nie wiemy, czy ma ono thenComparingmetodę itp.

Istnieje kilka sposobów rozwiązania tego problemu, z których wszystkie obejmują wstrzyknięcie większej ilości informacji o typie, aby można było poprawnie wpisać początkowy obiekt w łańcuchu. Oto one, w przybliżonej kolejności malejącej atrakcyjności i rosnącej inwazyjności:

  • Użyj dokładnego odwołania do metody (bez przeciążeń), na przykład Song::getTitle. To daje wystarczającą ilość informacji o typie, aby wywnioskować zmienne typu dla comparing()wywołania, a zatem nadać mu typ, a zatem kontynuować w dół łańcucha.
  • Użyj jawnej lambdy (tak jak w swoim przykładzie).
  • Zapewniamy typu świadkiem comparing()rozmowy: Comparator.<Song, String>comparing(...).
  • Podaj jawny typ docelowy z rzutowaniem, rzutując wyrażenie odbiornika na Comparator<Song>.
Brian Goetz
źródło
13
+1 za faktyczną odpowiedź na OP „dlaczego kompilator nie może tego wywnioskować”, zamiast po prostu podać obejścia / rozwiązania.
Joffrey,
Dziękuję za odpowiedź Brian. Jednak wciąż znajduję coś bez odpowiedzi, dlaczego List.sort zachowuje się inaczej niż Collections.sort, ponieważ ta pierwsza wymaga tylko pierwszej lambdy, aby zawierała typ parametru, ale druga wymaga również ostatniej, np. Jeśli mam porównanie następnie 5, a następnie Porównując wywołania, musiałbym umieścić (Pieśń p1) w porównaniu, a następnie w ostatnim wtedy Porównaniu. Również w moim oryginalnym poście zobaczysz dolny przykład TreeSet, w którym usuwam wszystkie typy parametrów, a ostatnie wywołanie thenComparing jest w porządku, ale inne nie - więc zachowuje się inaczej.
Tranquility
3
@ user3780370 Czy nadal używasz kompilatora Eclipse? Nie widziałem tego zachowania, jeśli dobrze rozumiem twoje pytanie. Czy możesz (a) wypróbować go z javac z JDK 8 i (b) jeśli nadal się nie powiedzie, opublikuj kod?
Brian Goetz
@BrianGoetz Dzięki za tę sugestię. Właśnie skompilowałem go w oknie poleceń przy użyciu javac i kompiluje się tak, jak powiedziałeś. Wygląda na to, że jest to problem Eclipse. Nie zaktualizowałem jeszcze do Eclipse Luna, który jest specjalnie zbudowany dla JDK8, więc mam nadzieję, że może to zostać naprawione. Właściwie mam zrzut ekranu, który pokazuje, co działo się w Eclipse, ale nie wiem, jak tutaj pisać.
Tranquility
2
Myślę, że masz na myśli Comparator.<Song, String>comparing(...).
shmosel
23

Problemem jest wnioskowanie typu. Bez dodania (Song s)do pierwszego porównania comparator.comparingnie zna typu danych wejściowych, więc domyślnie przyjmuje wartość Object.

Możesz rozwiązać ten problem na 1 z 3 sposobów:

  1. Użyj nowej składni odwołania do metody Java 8

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  2. Wyciągnij każdy krok porównania do lokalnego odniesienia

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

    EDYTOWAĆ

  3. Wymuszanie typu zwracanego przez komparator (pamiętaj, że potrzebujesz zarówno typu danych wejściowych, jak i typu klucza porównania)

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

Myślę, że „ostatni” thenComparingbłąd składni wprowadza Cię w błąd. W rzeczywistości jest to problem z typem w całym łańcuchu, po prostu kompilator zaznacza tylko koniec łańcucha jako błąd składniowy, ponieważ myślę, że wtedy ostateczny typ zwracany nie pasuje.

Nie jestem pewien, dlaczego Listwykonuje lepsze zadanie wnioskowania niż Collectionskoro powinien wykonywać ten sam typ przechwytywania, ale najwyraźniej nie.

dkatzel
źródło
Dlaczego wie to dla rozwiązania, ArrayListale nie dla Collectionsrozwiązania (biorąc pod uwagę, że pierwsze wywołanie w łańcuchu ma Songparametr)?
Sotirios Delimanolis
4
Dziękuję za odpowiedź, jednak jeśli przeczytasz mój post, zobaczysz, że powiedziałem: „Zanim zaczniemy, wiem, że mógłbym użyć referencji do metod, np. Song :: getTitle, aby rozwiązać swoje problemy, ale moje zapytanie tutaj nie jest tak dużo coś, co chcę naprawić, ale coś, na co chcę odpowiedzi, tj. dlaczego kompilator Java obsługuje to w ten sposób ”.
Tranquility
Chcę odpowiedzi, dlaczego kompilator zachowuje się w ten sposób, gdy używam wyrażeń lambda. Akceptuje porównywanie (s -> s.getArtist ()), ale kiedy na przykład łączę łańcuch .thenComparing (s -> s.getDuration ()), daje mi błędy składniowe dla obu wywołań, jeśli następnie dodam jawny typ w wywołanie porównania, np. comparing ((Song s) -> s.getArtist ()) to naprawia ten problem, a dla List.sort i TreeSet rozwiązuje również wszystkie dalsze błędy kompilacji bez konieczności dodawania dodatkowych typów parametrów, jednakże dla Kolekcje.sort & Objects.com przygotowują przykłady do końca Porównanie nadal się nie udaje
Tranquility
1

Inny sposób radzenia sobie z tym błędem czasu kompilacji:

Prześlij jawnie zmienną swojej pierwszej funkcji porównującej, a następnie gotowe. Posortowałem listę obiektu org.bson.Documents. Proszę spojrzeć na przykładowy kod

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
źródło
0

playlist1.sort(...) tworzy granicę Song dla zmiennej typu E, z deklaracji playlist1, która „faluje” do komparatora.

W programie Collections.sort(...)nie ma takiego ograniczenia, a wnioskowanie z typu pierwszego komparatora nie jest wystarczające, aby kompilator wywnioskował resztę.

Myślę, że uzyskałbyś „poprawne” zachowanie Collections.<Song>sort(...), ale nie masz instalacji java 8, aby to przetestować.

amaloy
źródło
cześć, tak, masz rację, dodając Kolekcje. <Śpiew> usuwa błąd z ostatniego wywołania porównawczego
Tranquility