.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класса.

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


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>.


Не совсем правильно говорить, что Integer::minреализует Comparable. Это не тот тип, который может реализовать что угодно. Но это оценивается в объект, который реализует Comparable.
— Лии

1
@Lii Спасибо, я исправил это только сейчас.
— Скиви

Comparator<Integer>является интерфейсом с одним абстрактным методом (он же «функциональный») и Integer::minвыполняет свой контракт, поэтому лямбда-выражение можно интерпретировать следующим образом. Я не знаю, как вы видите, как здесь вступает в игру BinaryOperator (или IntBinaryOperator) - между этим и Comparator нет никакой подтиповой связи.
— Пауло Эберманн

2

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

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

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

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

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


1

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

int max = Arrays.stream(arrayWithInts).max().getAsInt();
int min = Arrays.stream(arrayWithInts).min().getAsInt();
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.