Тип аргумента функции 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>