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

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

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

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

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

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

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

  1. статический метод (ClassName::methodName)
  2. метод экземпляра определенного объекта (instanceRef::methodName)
  3. супер метод конкретного объекта (super::methodName)
  4. метод экземпляра произвольного объекта определенного типа (ClassName::methodName)
  5. ссылка на конструктор класса (ClassName::new)
  6. ссылка на конструктор массива (TypeName[]::new)

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

я считаю, что следующие примеры иллюстрируют это дальше. А 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 ссылается на следующий отрывок из Брайана Гетца отличный документ, который описывает подробнее о переводах лямбда.

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

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

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

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

Итак, лямбды - desugared на новый метод. Е. Г.

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

код выше будет desugared что-то вроде этого:

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

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

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

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

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

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

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

Edit: после ввода из OP я добавил низкий уровень пример это объясняет результате обессахаривания