Распознавание типов в компараторе Java

Рейтинг: 4Ответов: 1Опубликовано: 15.03.2023

Пусть есть некий класс Test, содержащий поле field типа int:

class Test {
    private final int field;

    public int getField() {
        return field;
    }

    public Test(int field) {
        this.field = field;
    }

    @Override
    public String toString() {
        return String.valueOf(field);
    }
}

Есть список List<Test> list объектов класса Test, который мы хотим отсортировать посредством компаратора по полю field.

Компаратор для такой сортировки может выглядеть так:

list.sort(Comparator.comparingInt(test -> test.getField()));

Понятно, что тут можно сократить до "method reference", но об этом речь позже.

Метод comparingInt() ожидает на вход тип ToIntFunction<? super T>, где T в нашем случае является классом Test. Очевидно, что в этой лямбде тип test распознаётся как Test, мы спокойно вызываем его метод getField() и получаем корректную лямбду.

Но если написать вот так:

list.sort(Comparator.comparingInt(test -> test.getField()).reversed());

то компилятор будет ругаться, говоря, что test имеет тип Object, в котором, разумеется, метода getField() нет.

Убрать эту ошибку можно тремя разными способами:

  • заместо лямбды сделать "method reference"
  • оставить лямбду, но привести её к типу ToIntFunction<Test>
  • оставить лямбду, но явно указать в ней тип переменной test как Test

Не считая, конечно, того варианта, когда мы создаём свой отдельный класс, реализующий Comparator<Test>.


Собственно, вопрос: почему вообще возникает такая ошибка? Из-за чего получается так, что при последующем вызове метода reversed() Java отказывается распознавать тип test как Test?
Причём такой же эффект возникает, если к создаваемому компаратору пытаться применять, например, метод thenComparing().

Ответы

▲ 1Принят

Перевод ответа: Why does type inference fail for lambda, but succeed for equivalent method reference?:

Почему извлечение типа не работает для лямбды, но успешно отрабатывает для эквивалентной ссылки на метод?

Из спецификации языка JLS §15.13.2:

В отличие от лямбда-выражения, ссылка на метод может быть конгруэнтной (соответствующей) типу обобщенной функции (то есть функции, имеющей параметры типа). Это связано с тем, что лямбда-выражение должно иметь возможность объявлять параметры типа, и ни один синтаксис это не поддерживает; в то же время для ссылки на метод такое объявление типа не требуется.

Лямбда выражение вызывает ошибку компиляции поскольку не указан явно аргумент типа...

Что касается ссылок на метод, то поскольку тип может быть извлечён из параметров метода, явный аргумент типа не нужен, как указано выше в спецификации языка.


Это известное ограничение в механизме извлечения типа, который недостаточно "силён", чтобы сделать сразу два шага.

list.sort(Comparator.comparingInt(test -> test.getField())); // OK
list.sort(Comparator.comparingInt(test -> test.getField()).reversed()); // FAIL
list.sort(Comparator.comparingInt(Test::getField).reversed()); // ОК

Чтобы извлечь тип test в лямбде, должен быть определён целевой тип лямбды.
В первом случае это просто, так как метод list.sort ожидает в качестве параметра Comparator<Test>, который успешно возвращается статическим методом Comparator.comparingInt: Comparator<Test> comparingInt(t -> t.getField()). В свою очередь, comparingInt ожидает функцию, преобразующую тип Test в int, то есть переменная test в лямбде должна иметь тип Test.

При вызове обобщённого метода reversed() после лямбды с неявной типизацией этот механизм "ломается", так как в этом контексте недостаточно информации для определения целевого типа лямбды. Здесь результат comparingInt передаётся дальше по цепочке в reversed(), который также возвращает некий тип Comparator<T> для текущего экземпляра компаратора, но целевой тип Test в данном случае не может быть передан назад из reversed. Поэтому в случае сомнений компилятор решает, что целевым типом является Object и возникает указанная ошибка.

При использовании же ссылки на метод информация о целевом типе присутствует явно, то есть, нет необходимости в дополнительном извлечении типа, и reversed() и принимает и отдает Comparator<Test>.

Соответственно, другие варианты записи, которые позволяют однозначно определить целевой тип, будут успешно компилироваться:

list.sort(Comparator.comparingInt((Test test) -> test.getField()).reversed()); // ОК
list.sort(Comparator.<Test>comparingInt(test -> test.getField()).reversed()); // ОК
list.sort(Comparator.<Test,Integer>comparing(test -> test.getField()).reversed()); // ОК

Эта проблема была обнаружена достаточно давно и обсуждается достаточно часто, см.: