Очень смущает вывод типа компаратора Java 8

84

Я изучал разницу между Collections.sortи list.sort, особенно в отношении использования Comparatorстатических методов и того, требуются ли типы параметров в лямбда-выражениях. Прежде чем мы начнем, я знаю, что могу использовать ссылки на методы, например, Song::getTitleдля решения моих проблем, но мой запрос здесь - это не столько то, что я хочу исправить, а то, на что я хочу получить ответ, т.е. почему компилятор Java обрабатывает это таким образом .

Это моя находка. Предположим, у нас есть ArrayListтип Song, с добавленными песнями, есть 3 стандартных метода получения:

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

Вот вызов обоих типов методов сортировки, которые работают без проблем:

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

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

Как только я начинаю цепочку thenComparing, происходит следующее:

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

т.е. синтаксические ошибки, потому что он больше не знает тип p1. Чтобы исправить это, я добавляю тип Songк первому параметру (сравнения):

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

А теперь - ЗАДАЧА. Для p laylist1.sort, то есть для списка, это решит все ошибки компиляции для обоих следующих thenComparingвызовов. Однако Collections.sortон решает его для первого, а не для последнего. Я тестировал добавление нескольких дополнительных вызовов, thenComparingи он всегда показывает ошибку для последнего, если я не поставил (Song p1)параметр.

Теперь я продолжил тестировать это, создав TreeSetи используя 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())
            );

То же самое, что и для TreeSet, нет ошибок компиляции, кроме Objects.compareпоследнего вызоваthenComparing показывает ошибку.

Может ли кто-нибудь объяснить, почему это происходит, а также почему нет необходимости использовать (Song p1) при простом вызове метода сравнения (без дальнейших thenComparingвызовов).

Еще один вопрос по той же теме - когда я делаю это с TreeSet:

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

т.е. удалите тип Songиз первого лямбда-параметра для вызова метода сравнения, он показывает синтаксические ошибки при вызове сравнения и первом вызове, thenComparingно не последнем вызове thenComparing- почти полная противоположность тому, что происходило выше! Принимая во внимание, что для всех остальных трех примеров, то есть с Objects.compare, List.sortи Collections.sortкогда я удаляю это первымSong тип паров он показывает ошибки синтаксиса для всех вызовов.

Спасибо заранее.

Отредактировано, чтобы включить снимок экрана с ошибками, которые я получал в Eclipse Kepler SR2, которые, как я обнаружил, специфичны для Eclipse, потому что при компиляции с использованием java-компилятора JDK8 в командной строке он компилируется нормально.

Сортировка ошибок в Eclipse

Спокойствие
источник
Было бы полезно, если бы вы включили в свой вопрос все сообщения об ошибках компиляции, которые вы получаете во всех своих тестах.
Eran
1
честно говоря, я думаю, что было бы проще для кого-то увидеть, в чем проблема, запустив исходный код самостоятельно.
Tranquility,
Какие типы t1и t2в Objects.compareпримере? Я пытаюсь вывести их, но сопоставить мой вывод типа над выводом типа компилятора невозможно. :-)
Stuart Marks
1
И какой компилятор вы используете?
Стюарт Маркс
1
У вас есть две отдельные проблемы. Один из респондентов указал, что вы можете использовать ссылки на методы, которые вы вроде как отмахнулись. Точно так же, как лямбда-выражения бывают как «явно типизированными», так и «неявно типизированными», ссылки на методы бывают «точными» (одна перегрузка) и «неточными» (несколько перегрузок). Либо точная ссылка на метод, либо явная лямбда может использоваться для предоставления дополнительной информации о типе, если она отсутствует. (Явные свидетели типа и слепки также могут быть использованы, но часто это более крупные молотки.)
Брайан Гетц

Ответы:

105

Во-первых, все примеры, которые вы называете причиной ошибок, отлично компилируются с эталонной реализацией (javac из JDK 8.) Они также отлично работают в IntelliJ, поэтому вполне возможно, что ошибки, которые вы видите, специфичны для Eclipse.

Ваш основной вопрос, кажется: «почему он перестает работать, когда я начинаю цепочку». Причина в том, что в то время как лямбда-выражения и вызовы универсальных методов являются поли-выражениями (их тип зависит от контекста), когда они появляются как параметры метода, когда они появляются вместо выражений приемника метода, это не так.

Когда ты говоришь

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

информации о типе достаточно для решения как аргумента типа, так comparing()и типа аргумента p1. comparing()Вызов получает целевой тип с подписьюCollections.sort , так как известно , comparing()должны возвращать Comparator<Song>, и , следовательно , p1должно быть Song.

Но когда вы начинаете цепочку:

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

теперь у нас проблема. Мы знаем, что у составного выражения comparing(...).thenComparing(...)есть целевой типComparator<Song> , но поскольку выражение-получатель для цепочки comparing(p -> p.getTitle())является вызовом общего метода, и мы не можем вывести параметры его типа из других аргументов, нам не повезло . Поскольку мы не знаем тип этого выражения, мы не знаем, что у него есть thenComparingметод и т. Д.

Есть несколько способов исправить это, и все они включают добавление дополнительной информации о типе, чтобы исходный объект в цепочке мог быть правильно типизирован. Вот они, в приблизительном порядке уменьшения желательности и увеличения навязчивости:

  • Используйте точную ссылку на метод (без перегрузок), например Song::getTitle. Затем это дает достаточно информации о типе, чтобы вывести переменные типа дляcomparing() вызова и, следовательно, присвоить ему тип и, следовательно, продолжить вниз по цепочке.
  • Используйте явную лямбду (как в своем примере).
  • Укажите тип свидетеля comparing()вызова:Comparator.<Song, String>comparing(...) .
  • Предоставьте явный целевой тип с приведением, приведя выражение получателя к Comparator<Song>.
Брайан Гетц
источник
13
+1 за фактический ответ на OP «почему компилятор не может вывести это», а не просто предоставление обходных путей / решений.
Джоффри
Спасибо за ответ, Брайан. Однако я все еще нахожу что-то без ответа, почему List.sort ведет себя иначе, чем Collections.sort, в том смысле, что для первого требуется только первая лямбда, содержащая тип параметра, но для последнего также требуется последний, например, если у меня есть сравнение с последующими 5 вызовами thenComparing, мне пришлось бы поместить (Song p1) в сравнение и в последний thenComparing. Также в моем исходном сообщении вы увидите нижний пример TreeSet, где я удаляю все типы параметров, но последний вызов thenComparing в порядке, но другие нет, поэтому это ведет себя по-другому.
Tranquility
3
@ user3780370 Вы все еще используете компилятор Eclipse? Я не видел такого поведения, если я правильно понял ваш вопрос. Можете ли вы (а) попробовать его с javac из JDK 8 и (б), если он все еще не работает, опубликуйте код?
Brian Goetz
@BrianGoetz Спасибо за это предложение. Я только что скомпилировал его в командном окне с помощью javac, и он компилируется, как вы сказали. Похоже, это проблема Eclipse. Я еще не обновился до Eclipse Luna, который специально создан для JDK8, так что, надеюсь, это может быть исправлено в этом. На самом деле у меня есть снимок экрана, чтобы показать вам, что происходило в Eclipse, но я не знаю, как публиковать здесь сообщения.
Tranquility,
2
Я думаю ты имеешь в виду Comparator.<Song, String>comparing(...).
shmosel
23

Проблема заключается в определении типа. Без добавления (Song s)к первому сравнению comparator.comparingне знает тип ввода, поэтому по умолчанию используется Object.

Вы можете решить эту проблему одним из трех способов:

  1. Используйте новый синтаксис ссылки на метод Java 8

     Collections.sort(playlist,
                Comparator.comparing(Song::getTitle)
                .thenComparing(Song::getDuration)
                .thenComparing(Song::getArtist)
                );
    
  2. Вытяните каждый шаг сравнения в локальную ссылку

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

    РЕДАКТИРОВАТЬ

  3. Принудительное использование типа, возвращаемого компаратором (обратите внимание, что вам нужен как тип ввода, так и тип ключа сравнения)

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

Я думаю, что "последняя" thenComparingсинтаксическая ошибка вводит вас в заблуждение. На самом деле это проблема типа для всей цепочки, это просто компилятор отмечает только конец цепочки как синтаксическую ошибку, потому что, я думаю, когда окончательный тип возврата не соответствует.

Я не уверен, почему Listэто делает работу логического вывода лучше, чем Collectionкогда он должен делать тот же тип захвата, но, по-видимому, нет.

Dkatzel
источник
Почему он знает это для решения, ArrayListа не для Collectionsрешения (учитывая, что первый вызов в цепочке имеет Songпараметр)?
Сотириос Делиманолис
4
Спасибо за ответ, однако, если вы прочтете мой пост, вы увидите, что я сказал: «Прежде чем мы начнем, я знаю, что могу использовать ссылки на методы, например Song :: getTitle, чтобы решить свои проблемы, но мой запрос здесь не так уж и велик. кое-что, что я хочу исправить, но кое-что, на что мне нужен ответ, например, почему компилятор Java обрабатывает это таким образом ».
Tranquility
Мне нужен ответ, почему компилятор так себя ведет, когда я использую лямбда-выражения. Он принимает сравнение (s -> s.getArtist ()), но затем, когда я связываю .thenComparing (s -> s.getDuration ()), например, он дает мне синтаксические ошибки для обоих вызовов, если я затем добавлю явный тип в вызов сравнения, например, comparing ((Song s) -> s.getArtist ()), то это устраняет эту проблему, а для List.sort и TreeSet он также решает все дальнейшие ошибки компиляции без необходимости добавления дополнительных типов параметров, однако для the Collections.sort & Objects.compare примеры последний thenComparing все еще не работает
Tranquility
1

Другой способ справиться с этой ошибкой времени компиляции:

Явно укажите переменную вашей первой функции сравнения, и тогда все готово. У меня есть отсортированный список объекта org.bson.Documents. Посмотрите пример кода

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

playlist1.sort(...) создает границу Song для переменной типа E из объявления playlist1, которая "передается" компаратору.

В Collections.sort(...), такой границы нет, и вывода из типа первого компаратора недостаточно для компилятора, чтобы вывести остальные.

Я думаю, вы получите "правильное" поведение Collections.<Song>sort(...), но у вас нет установки java 8, чтобы проверить это для вас.

сплав
источник
привет, да, вы правы в добавлении коллекций. <Song> действительно избавляет от ошибки для последнего вызова thenComparing
Tranquility