Подтвердить что ты не робот

Ссылка на методы с разными параметрами в Java8

Мне интересно, как все это с ссылками на методы и функциональными интерфейсами работает на более низком уровне. Самый простой пример - это где у нас есть список

List<String> list = new ArrayList<>();
list.add("b");
list.add("a");
list.add("c"):

Теперь мы хотим отсортировать его с помощью класса Collections, чтобы мы могли вызвать:

Collections.sort(list, String::compareToIgnoreCase);

Но если мы определим пользовательский компаратор, это может быть что-то вроде:

Comparator<String> customComp = new MyCustomOrderComparator<>();
Collections.sort(list, customComp::compare);

Проблема заключается в том, что Collections.sort принимает два параметра: List и Comparator. Поскольку Comparator является функциональным интерфейсом, его можно заменить на выражение lambda или ссылку на метод с той же сигнатурой (параметры и тип возврата). Итак, как это работает, мы можем передать также ссылку на compareTo, которая принимает только один параметр, а подписи этих методов не совпадают? Как ссылки на методы, переведенные в Java8?

4b9b3361

Ответ 1

Из Справочник по методам методов Oracle:

Ссылка на метод экземпляра произвольного объекта определенного типа

Ниже приведен пример ссылки на метод экземпляра произвольного объекта определенного типа:

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
   Arrays.sort(stringArray, String::compareToIgnoreCase);

Эквивалентное лямбда-выражение для ссылки метода String::compareToIgnoreCase будет иметь формальный список параметров (String a, String b), где a и b - произвольные имена, используемые для лучшего описания этого примера. Ссылка на метод вызовет метод a.compareToIgnoreCase(b).

Но что означает оператор ::? Ну, оператор :: может быть описан следующим образом (от этот вопрос SO):

Ссылка на метод может быть получена в разных стилях, но все они означают одно и то же:

  • Статический метод (ClassName::methodName)
  • Метод экземпляра конкретного объекта (instanceRef::methodName)
  • Супер метод конкретного объекта (super::methodName)
  • Метод экземпляра произвольного объекта определенного типа (ClassName::methodName)
  • Ссылка на конструктор класса (ClassName::new)
  • Ссылка на конструктор массива (TypeName[]::new)

Итак, это означает, что ссылка метода String::compareToIgnoreCase попадает под вторую категорию (instanceRef::methodName), что означает, что ее можно перевести на (a, b) -> a.compareToIgnoreCase(b).

Я считаю, что следующие примеры иллюстрируют это далее. A Comparator<String> содержит один метод, который работает с двумя операндами String и возвращает int. Это может быть псевдоописано как (a, b) ==> return int (где операнды a и b). Если вы просмотрите его таким образом, все перечисленные ниже подпадают под эту категорию:

// Trad anonymous inner class
// Operands: o1 and o2. Return value: int
Comparator<String> cTrad = new Comparator<String>() {
    @Override
    public int compare(final String o1, final String o2) {
        return o1.compareToIgnoreCase(o2);
    }
};

// Lambda-style
// Operands: o1 and o2. Return value: int
Comparator<String> cLambda = (o1, o2) -> o1.compareToIgnoreCase(o2);

// Method-reference à la bullet #2 above. 
// The invokation can be translated to the two operands and the return value of type int. 
// The first operand is the string instance, the second operand is the method-parameter to
// to the method compareToIgnoreCase and the return value is obviously an int. This means that it
// can be translated to "instanceRef::methodName".
Comparator<String> cMethodRef = String::compareToIgnoreCase;

Этот отличный SO-ответ объясняет, как скомпилированы лямбда-функции. В этом ответе Jarandinor ссылается на следующий отрывок из Брайана Гетца превосходного документа, в котором описывается больше о lambda translations.

Вместо генерации байт-кода для создания объекта, реализующего лямбда-выражение (например, вызов конструктора для внутреннего класса), мы описываем рецепт построения лямбда и делегируем фактическую конструкцию языку runtime. Этот рецепт закодирован в статических и динамических списках аргументов invokedynamic инструкции.

В основном это означает, что собственная среда выполнения решает, как перевести лямбда.

Брайан продолжает:

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

Итак, lambdas desugared в новый метод. Например.

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( s -> { System.out.println(s); } );
    }
}

Приведенный выше код будет скрыт примерно так:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( [lambda for lambda$1 as Consumer] );
    }

    static void lambda$1(String s) {
        System.out.println(s);
    }
}

Но, Брайан также объясняет это в документе:

если метод desugared является методом экземпляра, приемник считается первым аргументом

Брайан продолжает объяснять, что оставшиеся аргументы lambdas передаются как аргументы упомянутому методу.

Итак, с помощью этой записи Moandji Ezana десурагирование compareToIgnoreCase как Comparator<String> можно разбить на следующие шаги:

  • Collections#sort для a List<String> ожидает a Comparator<String>
  • Comparator<String> - это функциональный интерфейс с методом int sort(String, String), который эквивалентен BiFunction<String, String, Integer>
  • Таким образом, экземпляр-компаратор может быть снабжен BiFunction -собираемым lambda: (String a, String b) -> a.compareToIgnoreCase(b)
  • String::compareToIgnoreCase ссылается на метод экземпляра, который принимает аргумент String, поэтому он совместим с приведенным выше лямбда: String a становится получателем, а String b становится аргументом метода

Изменить: После ввода из OP я добавил пример низкого уровня, который объясняет desugaring