Тип аргумента функции Java 8 stream max() Comparator vs Comparable
Я написал простой код, как показано ниже. Этот класс отлично работает без каких-либо ошибок.
public class Test {
public static void main(String[] args) {
List<Integer> intList = IntStream.of(1,2,3,4,5,6,7,8,9,10).boxed().collect(Collectors.toList());
int value = intList.stream().max(Integer::compareTo).get();
//int value = intList.stream().max(<Comparator<? super T> comparator type should pass here>).get();
System.out.println("value :"+value);
}
}
Как видно из комментария к коду, метод max()
должен передавать аргумент типа Comparator<? super Integer>
Comparator<? super Integer>
.
Но Integer::compareTo
реализует интерфейс Comparable
не Comparator
.
public final class Integer extends Number implements Comparable<Integer> {
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
}
Как это может работать? Метод max()
говорит, что ему нужен аргумент Comparator
, но он работает с аргументом Comparable
.
Я знаю, что что-то неправильно понял, но теперь я знаю, что. Может кто-нибудь объяснить, пожалуйста?
Ответы
Ответ 1
int value = intList.stream().max(Integer::compareTo).get();
Приведенный фрагмент кода логически эквивалентен следующему:
int value = intList.stream().max((a, b) -> a.compareTo(b)).get();
Что также логически эквивалентно следующему:
int value = intList.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a.compareTo(b);
}
}).get();
Comparator
- это функциональный интерфейс, который можно использовать как лямбда или ссылку на метод, поэтому ваш код компилируется и выполняется успешно.
Я рекомендую прочитать учебное руководство Oracle по методике "Ссылки на методы" (они используют пример сравнения двух объектов), а также спецификацию языка Java на §15.13. Выражения из справочника по методам, чтобы понять, почему это работает.
Ответ 2
Я могу относиться к вашему замешательству.
У нас есть метод Comparator
который объявляет два параметра
int compare(T o1, T o2);
и у нас есть метод Integer
который принимает один параметр
int compareTo(Integer anotherInteger)
Как же Integer::compareTo
разрешается в экземпляр Comparator
?
Когда ссылка на метод указывает на метод экземпляра, анализатор может искать методы с арностью n-1
(n
- это ожидаемое количество параметров).
Вот выдержка из JLS о том, как идентифицируются применимые методы.Я опущу первую часть о разборе выражения перед символом ::
token.
Во-вторых, с учетом целевого типа функции с n
параметрами определяется набор потенциально применимых методов:
Если выражение ссылки на метод имеет вид ReferenceType :: [TypeArguments] Identifier
, то потенциально применимыми методами являются:
-
методы-члены типа для поиска, который был бы потенциально применим (§15.12.2.1) для вызова метода, который называет Идентификатор, имеет арность n, имеет аргументы типа TypeArguments и появляется в том же классе, что и выражение ссылки на метод; плюс
-
методы-члены типа для поиска, которые потенциально могли бы применяться для вызова метода, который называет Identifier
, имеет арность n-1, имеет аргументы типа TypeArguments и появляется в том же классе, что и выражение ссылки на метод.
Рассматриваются две разные арности, n
и n-1
, для учета возможности того, что эта форма ссылается либо на статический метод, либо на метод экземпляра.
...
Выражение ссылки на метод в виде ReferenceType :: [TypeArguments] Identifier
может интерпретироваться по-разному. Если Identifier
ссылается на метод экземпляра, то неявное лямбда-выражение имеет дополнительный параметр по сравнению с Identifier
ссылающимся на статический метод.
https://docs.oracle.com/javase/specs/jls/se12/html/jls-15.html#jls-15.13.1
Если бы мы должны были написать неявное лямбда-выражение из ссылки на этот метод, первый (неявный) параметр был бы экземпляром, для которого вызывался метод, а второй (явный) параметр был бы аргументом, передаваемым в метод.
(implicitParam, anotherInteger) -> implicitParam.compareTo(anotherInteger)
Обратите внимание, что ссылка на метод отличается от лямбда-выражения, хотя первое может быть легко преобразовано во второе. Лямбда-выражение должно быть выведено в новый метод, в то время как ссылка на метод обычно требует только загрузки соответствующего константного дескриптора метода.
Integer::compareTo
реализует Comparable
интерфейс - не Comparator
.
Integer::compareTo
как выражение не реализует никакого интерфейса. Однако он может ссылаться на/представлять различные функциональные типы, одним из которых является Comparator<Integer>
.
Comparator<Integer> a = Integer::compareTo;
BiFunction<Integer, Integer, Integer> b = Integer::compareTo;
ToIntBiFunction<Integer, Integer> c = Integer::compareTo;
Ответ 3
Integer
реализует Comparable
, переопределяя compareTo
.
Однако это переопределенное compareTo
может быть использовано способом, который удовлетворяет и реализует интерфейс Comparator
.
В его использовании здесь
int value = intList.stream().max(Integer::compareTo).get();
это переводится в нечто вроде
int value = intList.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
}).get();
Ссылка на метод (или лямбда-выражение) должна удовлетворять сигнатуре единственного абстрактного метода соответствующего функционального интерфейса и, в этом случае (Comparator
), compareTo
делает.
Идея состоит в том, что max
ожидает Comparator
а его метод compare
ожидает два объекта Integer
. Integer::compareTo
может удовлетворить эти ожидания, потому что он также ожидает два объекта Integer
. Первый - это его получатель (экземпляр, в котором должен вызываться метод), а второй - это аргумент. С новым синтаксисом Java 8 компилятор переводит один стиль в другой.
(compareTo
также возвращает int
как того требует Comparator#compare
.)
Ответ 4
Первый трюк: все методы экземпляра на самом деле принимают 1 дополнительный неявный аргумент, тот, на который вы ссылаетесь как this
в теле метода. Например:
public final class Integer extends Number implements Comparable<Integer> {
public int compareTo(/* Integer this, */ Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
}
Integer a = 10, b = 100;
int compareResult = a.compareTo(b);
// this actually 'compiles' to Integer#compareTo(this = a, anotherInteger = b)
Второй трюк: компилятор Java может "преобразовать" сигнатуру ссылки на метод в некоторый функциональный интерфейс, если количество и типы аргументов (включая this
) удовлетворяют:
interface MyInterface {
int foo(Integer bar, Integer baz);
}
Integer a = 100, b = 1000;
int result1 = ((Comparator<Integer>) Integer::compareTo).compare(a, b);
int result2 = ((BiFunction<Integer, Integer, Integer>) Integer::compareTo).apply(a, b);
int result3 = ((MyInterface) Integer::compareTo).foo(a, b);
// result1 == result2 == result3
Как вы можете видеть, class Integer
реализует ни один из Comparator
, BiFunction
или случайный MyInterface
, но это не мешает вам приводить ссылку на метод Integer::compareTo
качестве этих интерфейсов.
Ответ 5
Итак, если вы видите, Comparator является функциональным интерфейсом. Который можно заменить на лямбду. И Integer::compareTo
является ссылкой на метод.
@FunctionalInterface
public interface Comparator<T>