Ссылка на методы с различными параметрами в 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)
.
но, что значит ::
оператор действительно означает? Что ж,::
оператор может быть описан следующим образом (от это так вопрос):
ссылка на метод может быть получена в разных стилях, но все они означают одно и то же:
- статический метод (
ClassName::methodName
)- метод экземпляра определенного объекта (
instanceRef::methodName
)- супер метод конкретного объекта (
super::methodName
)- метод экземпляра произвольного объекта определенного типа (
ClassName::methodName
)- ссылка на конструктор класса (
ClassName::new
)- ссылка на конструктор массива (
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 я добавил низкий уровень пример это объясняет результате обессахаривания