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

215

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

Посмотрите этот код ( примечание: я знаю, что этот код не будет «работать» и его 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класса.

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

FGE
источник
6
Обратите внимание, что он не работает должным образом, он должен использовать Integer::compareвместо Integer::maxи Integer::min.
Кристофер Хаммарстрем
@ ChristofferHammarström Я знаю это; обратите внимание, как я сказал перед извлечением кода: «Я знаю, это абсурд»
fge
3
Я не пытался вас исправить, я говорю людям в целом. Вы сделали это звучащим так, как будто вы думали, что абсурдно то, что методы Integerне являются методами Comparator.
Христоффер Хаммарстрем

Ответы:

242

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

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

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

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

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

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

int xxx(Integer o1, Integer o2);

Я использую «ххх», потому что имя метода не используется для сопоставления .

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

Дэвид М. Ллойд
источник
28
или альтернативно: list.stream().mapToInt(i -> i).max().get().
assylias
13
@assylias Вы хотите использовать .getAsInt()вместо, get()хотя, как вы имеете дело с OptionalInt.
Скиви
... когда мы только пытаемся предоставить собственный компаратор для max()функции!
Manu343726
Стоит отметить, что этот «интерфейс SAM» на самом деле называется «функциональным интерфейсом» и что, глядя на Comparatorдокументацию, мы видим, что он украшен аннотацией @FunctionalInterface. Этот декоратор - это магия, которая позволяет Integer::maxи Integer::minпревращается в Comparator.
Крис Керекс
2
@ChrisKerekes декоратор @FunctionalInterfaceв основном только для целей документирования, так как компилятор может с радостью сделать это с любым интерфейсом с помощью одного единственного абстрактного метода.
errantlinguist
117

Comparatorявляется функциональным интерфейсом и Integer::maxсоответствует этому интерфейсу (после того, как принимается во внимание автобокс / распаковка). Он принимает два intзначения и возвращает int- так же, как вы ожидаете a 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());
Джон Скит
источник
1
Я знаю о функциональных интерфейсах; и я знаю, почему это дает неправильные результаты. Мне просто интересно, как,
черт возьми,
6
@fge: Ну, это была распаковка, о которой ты не знал? (Я не рассматривал именно эту часть.) А Comparator<Integer>было бы int compare(Integer, Integer)... это не ошеломляет, что Java позволяет ссылаться на метод int max(int, int)для преобразования в это ...
Джон Скит
7
@fge: должен ли компилятор знать семантику Integer::max? С его точки зрения, вы передали функцию, которая соответствует его спецификации, вот и все, на что она действительно способна.
Марк Питерс
6
@fge: В частности, если вы понимаете часть происходящего, но заинтригованы одним конкретным аспектом, стоит пояснить этот вопрос, чтобы люди не тратили свое время на объяснение уже известных вам фрагментов.
Джон Скит
20
Я думаю, что основной проблемой является сигнатура типа Comparator.compare. Она должна возвращать enumиз {LessThan, GreaterThan, Equal}, а не int. Таким образом, функциональный интерфейс на самом деле не будет совпадать, и вы получите ошибку компиляции. IOW: сигнатура типа Comparator.compareнеадекватно отражает семантику того, что означает сравнение двух объектов, и, следовательно, другие интерфейсы, которые абсолютно не имеют ничего общего со случайным сравнением объектов, имеют одинаковую сигнатуру типа.
Йорг Миттаг,
19

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

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

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

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

skiwi
источник
Не совсем правильно говорить, что Integer::minреализует Comparable. Это не тот тип, который может реализовать что угодно. Но это оценивается в объект, который реализует Comparable.
Лии
1
@Lii Спасибо, я исправил это только сейчас.
Скиви
Comparator<Integer>является интерфейсом с одним абстрактным методом (он же «функциональный») и Integer::minвыполняет свой контракт, поэтому лямбда-выражение можно интерпретировать следующим образом. Я не знаю, как вы видите, как здесь вступает в игру BinaryOperator (или IntBinaryOperator) - между этим и Comparator нет никакой подтиповой связи.
Пауло Эберманн
2

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

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

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

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

Посмотрите раздел Вывода типов в Учебном руководстве по Java для получения дополнительной информации.

Lii
источник
1

У меня была ошибка с массивом, получающим максимальное и минимальное значения, поэтому мое решение было:

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
JPRLCol
источник