Вступление
Лямбда-функции были дополнением, которое пришло с Java 8 , и были первым шагом языка к функциональному программированию , следуя общей тенденции к реализации полезных функций различных совместимых парадигм .
Мотивация для введения лямбда-функций заключалась в основном в том, чтобы уменьшить громоздкий повторяющийся код, который использовался для передачи экземпляров классов для имитации анонимных функций других языков.
Вот пример:
String[] arr = { "family", "illegibly", "acquired", "know", "perplexing", "do", "not", "doctors", "where", "handwriting", "I" };
Arrays.sort(arr, new Comparator<String>() {
@Override public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});
System.out.println(Arrays.toString(arr));
Как видите, создание экземпляра нового класса Comparator и переопределение его содержимого - это фрагмент повторяющегося кода, без которого мы тоже можем обойтись, поскольку он всегда один и тот же.
Всю Arrays.sort()
можно заменить чем-то более коротким и приятным, но
функционально эквивалентным:
Arrays.sort(arr, (s1,s2) -> s1.length() - s2.length());
Эти короткие и приятные фрагменты кода, которые делают то же самое, что и их подробные аналоги, называются синтаксическим сахаром . Это потому, что они не добавляют функциональности языку, а вместо этого делают его более компактным и читаемым. Лямбда-функции - это пример синтаксического сахара для Java.
Хотя я настоятельно рекомендую прочитать эту статью по порядку, если вы не знакомы с этой темой, вот краткий список того, что мы рассмотрим для более удобного использования:
Лямбды как объекты
Прежде чем мы перейдем к деталям самого синтаксиса лямбда, мы должны сначала взглянуть на то, что такое лямбда-функции и как они используются .
Как уже упоминалось, это просто синтаксический сахар, но это синтаксический сахар специально для объектов, реализующих единый интерфейс метода.
В этих объектах реализация лямбда считается реализацией указанного метода. Если лямбда и интерфейс совпадают, лямбда-функция может быть назначена переменной того типа интерфейса.
Согласование интерфейса с одним методом
Чтобы сопоставить лямбда-выражение с одним интерфейсом метода, также называемым «функциональным интерфейсом», необходимо выполнить несколько условий:
- Функциональный интерфейс должен иметь только один нереализованный метод, и этот метод (естественно) должен быть абстрактным. Интерфейс может содержать реализованные в нем статические методы и методы по умолчанию, но важно то, что существует только один абстрактный метод.
- Абстрактный метод должен принимать аргументы в том же порядке, который соответствует параметрам, которые принимает лямбда.
- Тип возвращаемого значения как метода, так и лямбда-функции должен совпадать.
Если все это выполнено, все условия для сопоставления выполнены, и вы можете назначить лямбду переменной.
Определим наш интерфейс:
public interface HelloWorld {
abstract void world();
}
Как видите, у нас довольно бесполезный функциональный интерфейс.
Он содержит ровно одну функцию, и эта функция может делать что угодно, если она не принимает аргументов и не возвращает значений.
Мы собираемся создать простую программу Hello World, используя это, хотя воображение - это предел, если вы хотите поиграть с ней:
public class Main {
public static void main(String[] args) {
HelloWorld hello = () -> System.out.println("Hello World!");
hello.world();
}
}
Как мы видим, если мы запустим это, наша лямбда-функция успешно
сопоставлена с HelloWorld
, и hello
можно использовать для доступа к
его методу.
Идея заключается в том, что вы можете использовать лямбды везде, где в
противном случае вы бы использовали функциональные интерфейсы для
передачи функций. Если вы помните наш пример Comparator
Comparator<T>
на самом деле является функциональным интерфейсом,
реализующим единственный метод - compare()
.
Вот почему мы могли бы заменить его лямбдой, которая ведет себя аналогично этому методу.
Выполнение
Основная идея лямбда-функций такая же, как и основная идея методов - они принимают параметры и используют их в теле, состоящем из выражений.
Реализация немного отличается. Возьмем пример лямбда сортировки String
(s1,s2) -> s1.length() - s2.length()
Его синтаксис можно понять как:
parameters -> body
Параметры
Параметры такие же, как параметры функции, это значения, передаваемые лямбда-функции, чтобы она могла что-то делать.
Параметры обычно заключаются в квадратные скобки и разделяются запятыми, хотя в случае лямбды, которая принимает только один параметр, скобки можно опустить.
Лямбда-функция может принимать любое количество параметров, включая ноль, поэтому у вас может получиться что-то вроде этого:
() -> System.out.println("Hello World!")
Эта лямбда-функция при сопоставлении с соответствующим интерфейсом будет работать так же, как следующая функция:
static void printing(){
System.out.println("Hello World!");
}
Точно так же у нас могут быть лямбда-функции с одним, двумя или несколькими параметрами.
Классический пример функции с одним параметром работает с каждым
элементом коллекции в цикле forEach
public class Main {
public static void main(String[] args) {
LinkedList<Integer> childrenAges = new LinkedList<Integer>(Arrays.asList(2, 4, 5, 7));
childrenAges.forEach( age -> System.out.println("One of the children is " + age + " years old."));
}
}
Здесь единственный параметр - age
. Обратите внимание, что мы удалили
круглые скобки здесь, потому что это разрешено, когда у нас есть только
один параметр.
Использование большего количества параметров работает аналогично, они
просто разделяются запятой и заключаются в круглые скобки. Мы уже видели
двухпараметрическую лямбду, когда сравнивали ее с Comparator
для
сортировки строк.
Тело
Тело лямбда-выражения состоит из одного выражения или блока операторов.
Если вы укажете только одно выражение в качестве тела лямбда-функции (будь то в блоке операторов или отдельно), лямбда автоматически вернет оценку этого выражения.
Если у вас есть несколько строк в вашем блоке операторов или если вы просто хотите (это свободная страна), вы можете явно использовать оператор return из блока операторов:
// just the expression
(s1,s2) -> s1.length() - s2.length()
// statement block
(s1,s2) -> { s1.length() - s2.length(); }
// using return
(s1,s2) -> {
s1.length() - s2.length();
return; // because forEach expects void return
}
Вы можете попробовать заменить любой из них в нашем примере сортировки в начале статьи, и вы обнаружите, что все они работают одинаково.
Переменный захват
Захват переменных позволяет лямбда-выражениям использовать переменные, объявленные вне самой лямбды.
Есть три очень похожих типа захвата переменных:
- захват локальной переменной
- захват переменной экземпляра
- захват статической переменной
Синтаксис почти идентичен тому, как вы могли бы получить доступ к этим переменным из любой другой функции, но условия, при которых вы можете это сделать, отличаются.
Вы можете получить доступ к локальной переменной, только если она является окончательной , что означает, что она не меняет своего значения после присвоения. Его не обязательно явно объявлять окончательным, но желательно сделать это, чтобы избежать путаницы. Если вы используете его в лямбда-функции, а затем измените его значение, компилятор начнет ныть.
Причина, по которой вы не можете этого сделать, заключается в том, что лямбда не может надежно ссылаться на локальную переменную, потому что она может быть уничтожена до того, как вы выполните лямбда. Из-за этого он делает глубокую копию . Изменение локальной переменной может привести к некоторой путанице, поскольку программист может ожидать, что значение в лямбде изменится, поэтому во избежание путаницы это явно запрещено.
Когда дело доходит до переменных экземпляра , если ваша лямбда
находится в том же классе, что и переменная, к которой вы обращаетесь,
вы можете просто использовать this.field
для доступа к полю в этом
классе. Более того, поле не обязательно должно быть окончательным и
может быть изменено позже в ходе программы.
Это связано с тем, что, если лямбда определена внутри класса, она создается вместе с этим классом и привязана к этому экземпляру класса и, таким образом, может легко ссылаться на значение поля, которое ему нужно.
Статические переменные фиксируются так же, как переменные
экземпляра, за исключением того факта, что вы не будете использовать
this
для ссылки на них. Они могут быть изменены и не обязательно
должны быть окончательными по тем же причинам.
Ссылка на метод
Иногда лямбда-выражения являются просто заменой определенного метода. Чтобы сделать синтаксис коротким и понятным, вам на самом деле не нужно вводить весь синтаксис, когда это так. Например:
s -> System.out.println(s)
эквивалентно:
System.out::println
::
сообщает компилятору, что вам просто нужна лямбда, которая передает
данный аргумент в println
. Вы всегда просто ставите перед именем
метода ::
где вы пишете лямбда-функцию, в противном случае вы
получаете доступ к методу как обычно, то есть вам все равно нужно
указать класс владельца перед двойным двоеточием.
В зависимости от типа вызываемого метода существуют различные типы ссылок на методы:
- ссылка на статический метод
- ссылка на метод параметра
- ссылка на метод экземпляра
- ссылка на метод конструктора
Справочник по статическим методам
Нам понадобится интерфейс:
public interface Average {
abstract double average(double a, double b);
}
Статическая функция:
public class LambdaFunctions {
static double averageOfTwo(double a, double b){
return (a+b)/2;
}
}
И наша лямбда-функция и вызов в main
:
Average avg = LambdaFunctions::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Справочник по методу параметра
Опять же, мы набираем main
.
Comparator<Double> cmp = Double::compareTo;
Double a = 20.3;
System.out.println(cmp.compare(a, 4.5));
Double::compareTo
эквивалентна:
Comparator<Double> cmp = (a, b) -> a.compareTo(b)
Справочник по методу экземпляра
Если мы возьмем наш LambdaFunctions
и нашу функцию averageOfTwo
(из
Справочника статических методов ) и сделаем их
нестатическими, мы получим следующее:
public class LambdaFunctions {
double averageOfTwo(double a, double b){
return (a+b)/2;
}
}
Чтобы получить к нему доступ, теперь нам нужен экземпляр класса, поэтому
нам нужно сделать это в main
:
LambdaFunctions lambda = new LambdaFunctions();
Average avg = lambda::averageOfTwo;
System.out.println(avg.average(20.3, 4.5));
Справочник по методам конструктора
Если у нас есть класс MyClass
и мы хотим вызвать его конструктор через
лямбда-функцию, наша лямбда будет выглядеть так:
MyClass::new
Он будет принимать столько аргументов, сколько может соответствовать одному из конструкторов.
Заключение
В заключение, лямбды - это полезная функция, позволяющая сделать наш код более простым, короче и читабельным.
Некоторые люди избегают их использования, когда в команде много юниоров, поэтому я бы посоветовал проконсультироваться с вашей командой, прежде чем проводить рефакторинг всего кода, но когда все находятся на одной странице, это отличный инструмент.
Смотрите также
Вот несколько дополнительных сведений о том, как и где применять лямбда-функции: