Среди новшеств, которые были привнесены в язык Java с выходом JDK 8, особняком стоят лямбда-выражения. Лямбда представляет набор инструкций, которые можно выделить в отдельную переменную и затем многократно вызвать в различных местах программы.
Основу лямбда-выражения составляет лямбда-оператор, который представляет стрелку ->. Этот оператор разделяет лямбда-выражение на две части: левая часть содержит список параметров выражения, а правая собственно представляет тело лямбда-выражения, где выполняются все действия.
Лямбда-выражение не выполняется само по себе, а образует реализацию метода, определенного в функциональном интерфейсе. При этом важно, что функциональный интерфейс должен содержать только один единственный метод без реализации.
Рассмотрим пример:
public class LambdaApp { public static void main(String[] args) { Operationable operation; operation = (x,y)->x+y; int result = operation.calculate(10, 20); System.out.println(result); //30 } } interface Operationable{ int calculate(int x, int y); }
В роли функционального интерфейса выступает интерфейс Operationable
, в котором определен один метод без реализации - метод
calculate
. Данный метод принимает два параметра - целых числа, и возвращает некоторое целое число.
По факту лямбда-выражения являются в некотором роде сокращенной формой внутренних анонимных классов, которые ранее применялись в Java. В частности, предыдущий пример мы можем переписать следующим образом:
public class LambdaApp { public static void main(String[] args) { Operationable op = new Operationable(){ public int calculate(int x, int y){ return x + y; } }; int z = op.calculate(20, 10); System.out.println(z); // 30 } } interface Operationable{ int calculate(int x, int y); }
Чтобы объявить и использовать лямбда-выражение, основная программа разбивается на ряд этапов:
Определение ссылки на функциональный интерфейс:
Operationable operation;
Создание лямбда-выражения:
operation = (x,y)->x+y;
Причем параметры лямбда-выражения соответствуют параметрам единственного метода интерфейса Operationable, а результат соответствует возвращаемому результату
метода интерфейса. При этом нам не надо использовать ключевое слово return
для возврата результата из лямбда-выражения.
Так, в методе интерфейса оба параметра представляют тип int
, значит, в теле лямбда-выражения мы можем применить к ним сложение. Результат сложения также представляет
тип int
, объект которого возвращается методом интерфейса.
Использование лямбда-выражения в виде вызова метода интерфейса:
int result = operation.calculate(10, 20);
Так как в лямбда-выражении определена операция сложения параметров, результатом метода будет сумма чисел 10 и 20.
При этом для одного функционального интерфейса мы можем определить множество лямбда-выражений. Например:
Operationable operation1 = (int x, int y)-> x + y; Operationable operation2 = (int x, int y)-> x - y; Operationable operation3 = (int x, int y)-> x * y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate(20, 10)); //10 System.out.println(operation3.calculate(20, 10)); //200
Одним из ключевых моментов в использовании лямбд является отложенное выполнение (deferred execution). То есть мы определяем в одном месте программы лямбда-выражение и затем можем его вызывать при необходимости неопределенное количество раз в различных частях программы. Отложенное выполнение может потребоваться, к примеру, в следующих случаях:
Выполнение кода отдельном потоке
Выполнение одного и того же кода несколько раз
Выполнение кода в результате какого-то события
Выполнение кода только в том случае, когда он действительно необходим и если он необходим
Параметры лямбда-выражения должны соответствовать по типу параметрам метода из функционального интерфейса. При написании самого лямбда-выражения тип параметров писать необязательно, хотя в принципе это можно сделать, например:
operation = (int x, int y)->x+y;
Если метод не принимает никаких параметров, то пишутся пустые скобки, например:
()-> 30 + 20;
Если метод принимает только один параметр, то скобки можно опустить:
n-> n * n;
Выше мы рассмотрели лямбда-выражения, которые возвращают определенное значение. Но также могут быть и терминальные лямбды, которые не возвращают никакого значения. Например:
interface Printable{ void print(String s); } public class LambdaApp { public static void main(String[] args) { Printable printer = s->System.out.println(s); printer.print("Hello Java!"); } }
Лямбда-выражение может использовать переменные, которые объявлены во вне в более общей области видимости - на уровне класса или метода, в котором лямбда-выражение определено. Однако в зависимости от того, как и где определены переменные, могут различаться способы их использования в лямбдах. Рассмотрим первый пример - использования переменных уровня класса:
public class LambdaApp { static int x = 10; static int y = 20; public static void main(String[] args) { Operation op = ()->{ x=30; return x+y; }; System.out.println(op.calculate()); // 50 System.out.println(x); // 30 - значение x изменилось } } interface Operation{ int calculate(); }
Переменные x и y объявлены на уровне класса, и в лямбда-выражении мы их можем получить и даже изменить. Так, в данном случае после выполнения выражения изменяется значение переменной x.
Теперь рассмотрим другой пример - локальные переменные на уровне метода:
public static void main(String[] args) { int n=70; int m=30; Operation op = ()->{ //n=100; - так нельзя сделать return m+n; }; // n=100; - так тоже нельзя System.out.println(op.calculate()); // 100 }
Локальные переменные уровня метода мы также можем использовать в лямбдах, но изменять их значение нельзя. Если мы попробуем это сделать,
то среда разработки (Netbeans) может нам высветить ошибку и то, что такую переменную надо пометить с помощью ключевого слова final,
то есть сделать константой: final int n=70;
. Однако это необязательно.
Более того, мы не сможем изменить значение переменной, которая используется в лямбда-выражении, вне этого выражения. То есть даже если такая переменная не объявлена как константа, по сути она является константой.
Существуют два типа лямбда-выражений: однострочное выражение и блок кода. Примеры однострочных выражений демонстрировались выше.
Блочные выражения обрамляются фигурными скобками. В блочных лямбда-выражениях можно использовать внутренние вложенные блоки, циклы, конструкции if, switch,
создавать переменные и т.д. Если блочное лямбда-выражение должно возвращать значение, то явным образом применяется оператор return
:
Operationable operation = (int x, int y)-> { if(y==0) return 0; else return x/y; }; System.out.println(operation.calculate(20, 10)); //2 System.out.println(operation.calculate(20, 0)); //0
Функциональный интерфейс может быть обобщенным, однако в лямбда-выражении использование обобщений не допускается. В этом случае нам надо типизировать объект интерфейса определенным типом, который потом будет применяться в лямбда-выражении. Например:
public class LambdaApp { public static void main(String[] args) { Operationable<Integer> operation1 = (x, y)-> x + y; Operationable<String> operation2 = (x, y) -> x + y; System.out.println(operation1.calculate(20, 10)); //30 System.out.println(operation2.calculate("20", "10")); //2010 } } interface Operationable<T>{ T calculate(T x, T y); }
Таким образом, при объявлении лямбда-выражения ему уже известно, какой тип параметры будут представлять и какой тип они будут возвращать.