Java 8 stream.min() и .max(): зачем это компилируется?

Примечание: этот вопрос исходит из мертвой ссылки, которая была предыдущим вопросом SO, но здесь идет...

Смотрите этот код (note: Я знаю, что этот код не будет работать и что Integer::compare должен использоваться - я просто извлек его из связанного вопроса):

final ArrayList <Integer> list 
    = IntStream.rangeClosed(1, 20).boxed().collect(Collectors.toList());

System.out.println(list.stream().max(Integer::max).get());
System.out.println(list.stream().min(Integer::min).get());

В соответствии с javadoc .min() и .max(), аргумент обоих должен быть Comparator. Однако здесь ссылки на методы относятся к статическим методам класса Integer.

Итак, почему эта компиляция вообще?

Ответ 1

Позвольте мне объяснить, что здесь происходит, потому что это не очевидно!

Во-первых, Stream.max() принимает экземпляр Comparator, чтобы элементы в потоке могли сравниваться друг с другом, чтобы найти минимум или максимум в каком-то оптимальном порядке, о котором вам не нужно слишком беспокоиться.

Итак, вопрос, конечно, в том, почему принят Integer::max? Ведь это не компаратор!

Ответ заключается в том, что новая функциональность лямбды работает в Java 8. Она опирается на концепцию, которая неофициально известна как интерфейсы с одним абстрактным методом или интерфейсы SAM. Идея состоит в том, что любой интерфейс с одним абстрактным методом может быть автоматически реализован с помощью любой привязки лямбда - или метода, чья сигнатура метода соответствует одному методу интерфейса. Поэтому рассмотрим интерфейс Comparator (простая версия):

public Comparator<T> {
    int compare(T o1, T o2);
}

Если метод ищет Comparator<Integer>, то он в основном ищет эту подпись:

int xxx(Integer o1, Integer o2);

Я использую "xxx" , потому что имя метода не используется для целей сопоставления.

Следовательно, как Integer.min(int a, int b), так и Integer.max(int a, int b) достаточно близки, чтобы автобоксинг позволял отображать это как Comparator<Integer> в контексте метода.

Ответ 2

Comparator - это функциональный интерфейс, а Integer::max соответствует этому интерфейсу (после учета автобоксинга/распаковки). Он принимает два значения int и возвращает int - так же, как вы ожидали бы Comparator<Integer> (опять же, прищурившись, чтобы игнорировать разницу в Integer/int).

Однако я не ожидал, что он поступит правильно, учитывая, что Integer.max не соответствует семантике Comparator.compare. И действительно, это вообще не работает. Например, сделайте одно небольшое изменение:

for (int i = 1; i <= 20; i++)
    list.add(-i);

... и теперь значение max равно -20, а значение min равно -1.

Вместо этого оба вызова должны использовать Integer::compare:

System.out.println(list.stream().max(Integer::compare).get());
System.out.println(list.stream().min(Integer::compare).get());

Ответ 3

Это работает, потому что Integer::min разрешает реализацию интерфейса Comparator<Integer>.

Ссылка метода Integer::min разрешается до Integer.min(int a, int b), разрешена к IntBinaryOperator, и предположительно автобоксинг происходит где-то, делая его BinaryOperator<Integer>.

И методы min() resp max() Stream<Integer> задают интерфейс Comparator<Integer>, который будет реализован.
Теперь это разрешает единственный метод Integer compareTo(Integer o1, Integer o2). Который имеет тип BinaryOperator<Integer>.

Таким образом, волшебство произошло, поскольку оба метода - это BinaryOperator<Integer>.

Ответ 4

Помимо информации, предоставленной Дэвидом М. Ллойдом, можно добавить, что механизм, который позволяет это, называется целевым типом.

Идея заключается в том, что тип, который компилятор присваивает лямбда-выражениям или ссылкам метода, не зависит только от самого выражения, но также от того, где он используется.

Цель выражения - это переменная, которой назначен ее результат, или параметр, которому передается его результат.

Лямбда-выражениям и методам присваивается тип, который соответствует типу их цели, если такой тип можно найти.

Дополнительную информацию см. в разделе ввода типов в учебнике Java.