it-swarm.com.ru

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

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

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

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

Теперь здесь прибывает КОНФУЗИОННАЯ часть. Для playlist1.sort, то есть для List, это решает все ошибки компиляции для обоих следующих вызовов 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 в командной строке он компилируется нормально. 

Sort errors in Eclipse

65
Tranquility

Во-первых, все примеры, о которых вы говорите, приводят к тому, что ошибки хорошо компилируются с эталонной реализацией (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>
79
Brian Goetz

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

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

  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, поскольку он должен делать тот же тип захвата, но, очевидно, нет.

19
dkatzel

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

Приведите явную переменную вашей первой функции сравнения, и тогда все готово. Я отсортировал список объекта 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
Rajni Gangwar

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

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

Я думаю, что вы получите «правильное» поведение из Collections.<Song>sort(...), но у вас не будет установлен Java 8, чтобы проверить его для вас.

0
amalloy