.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(), аргументом обох має бути a 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);

Я використовую "xxx", оскільки ім'я методу не використовується для відповідності .

Тому обидва 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.
skiwi

... коли те, що ми лише намагаємось, - це забезпечити спеціальний компаратор для max()функції!
Manu343726

Варто зазначити, що цей "SAM-інтерфейс" насправді називається "функціональним інтерфейсом" і що, переглядаючи Comparatorдокументацію, ми бачимо, що він прикрашений анотацією @FunctionalInterface. Цей декоратор - це магія, яка дозволяє Integer::maxі Integer::minперетворити його на Comparator.
Кріс Керекес

2
@ChrisKerekes декоратор @FunctionalInterfaceпризначений в основному лише для цілей документації, оскільки компілятор може радісно робити це за допомогою будь-якого інтерфейсу одним єдиним абстрактним методом.
блудниця

117

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

1
Я знаю про функціональні інтерфейси; і я знаю, чому це дає неправильні результати. Мені просто цікаво, як на землі компілятор не кричав на мене
1414

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не належним чином відображає семантику того, що означає порівняння двох об'єктів, і, таким чином, інші інтерфейси, які абсолютно не мають нічого спільного з порівнянням об'єктів, випадково мають підпис одного типу.
Йорг W Міттаг

19

Це працює, тому що Integer::minдозволена до реалізації Comparator<Integer>інтерфейсу.

Еталонний метод Integer::minвирішує до Integer.min(int a, int b), вирішив IntBinaryOperator, і , імовірно , Autoboxing відбувається де - то робить його BinaryOperator<Integer>.

І методи min()респ. Інтерфейсу запиту, який потрібно реалізувати. Тепер це вирішується до єдиного методу . Який тип .max()Stream<Integer>Comparator<Integer>
Integer compareTo(Integer o1, Integer o2)BinaryOperator<Integer>

І таким чином магія сталася, оскільки обидва способи є BinaryOperator<Integer>.


Не зовсім правильно сказати, що Integer::minреалізує Comparable. Це не тип, який може реалізувати що завгодно. Але він оцінюється в об'єкт, який реалізує Comparable.
Лій

1
@Lii Дякую, я це виправив саме зараз.
skiwi

Comparator<Integer>є інтерфейсом з єдиним абстрактним методом (він же "функціональний") і Integer::minвиконує свій контракт, тому лямбда можна інтерпретувати як це. Я не знаю, як ви бачите, як BinaryOperator вступає тут у гру (або IntBinaryOperator) - між цим порівнянням та компаратором немає взаємозв'язку підтипів.
Paŭlo Ebermann

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.