Лямбда выражения в Java 8

 Функциональные интерфейсы

В Java 8 появилась новая функциональная возможность - лямбда выражения. Это достаточно удобная фича, теперь можно не писать длинные анонимные классы с одним методом. Лямбда выражения можно использовать только с интерфейсами у которых есть лишь один абстрактный метод. Такие интерфейсы называются функциональными интерфейсами. В Java 8 есть много встроенных функциональных  интерфейсов. Находятся они в java.util.functional.

Создадим свой функциональный интерфейс.

interface MyFunctional {
boolean func(int a, int b);
}

Линейные лямбда выражения

Рассмотрим только что созданный интерфейс. Здесь мы создали обычный интерфейс который объявляет один абстрактный метод func. Этот интерфейс можно использовать как лямбда выражение. Давайте же сделаем это.

public class Main {

private static boolean execute(MyFunctional func, int a, int b) {
return func.func(a, b);
}

public static void main(String[] args) {
MyFunctional func = (a, b) -> a > b;
execute(func, 5, 10);
}

}

Пример не самый удачный, но чтобы понять концепцию этого достаточно. Здесь мы создаем лямбда выражение и сохраняем его в переменную func, обратите внимание что лямбда выражение преобразуется в интерфейс MyFunctional.

Рассмотрим само лямбда выражение.

(a, b) -> a > b;

Это линейное выражение. Есть еще блочное, о нем пойдет речь далее. В левой части (a, b) объявляются параметры которые может получать лямбда выражение, справа после -> объявляется тело лямбда выражения, когда мы его записываем в одну строку без знаков {}, то это означает то, что лямбда автоматически создает return statement. Выше приведенный код создает автоматически return.

(a, b) -> return a > b;

В линейных лямбдах return писать нельзя, это приведет к ошибке компиляции!

Обратите внимание на то, что мы не указываем типы переменных, они выводятся из параметров в методе func интерфейса MyFunctional, интерфейс лямбда выражения должен быть полностью совместим с интерфейсом абстрактного метода функционального интерфейса.

Параметры аргументов лямбда выражения можно указать явно.

(int a, int b) -> a > b;

Но это тоже необязательно, потому что это не дает никаких преимуществ. Есть одна особенность. Если вы указываете типы аргументов лямбда выражения, вы должны указать их для всех аргументов иначе получите ошибку компиляции. Смотрите пример.

(a, b) -> a > b; // Рабочее выражение
(int a, int b) -> a > b; // Рабочее выражение
(int a, b) -> a > b; // Ошибка компиляции

 Создадим новый функциональный интерфейс.

interface MyFunctional {
String func(String input);
}

А теперь давайте на основе этого интерфейса сделаем лямбда выражение которое будет переворачивать входную строку и возвращать ее. Ваших знаний должно быть уже достаточно чтобы решить эту задачу, напишите свои решения в комментариях.

public class Main {

private static String execute(MyFunctional func, String input) {
return func.func(input);
}

public static void main(String[] args) {
MyFunctional func = input -> new StringBuilder(input).reverse().toString();
String reverseMessage = execute(func, "Java");

System.out.println(reverseMessage);
}

}

Здесь мы создали лямбда выражение и сохранили его в переменную func. Обратите внимание что мы указали параметр без скобок, да, если параметр у лямбды один, то скобки можно опускать, но в дальнейших примерах они будут. Мы создали лямбду которая может перевернуть строку с помощью стандартного Java класса StringBuilder. Дальше мы передаем это лямбду в метод execute и параметр для нее в виде строки "Java".

В результате мы получим

avaJ

Лямбды не обязательно сохранять в переменные, их можно сразу передать в метод.

public class Main {

private static String execute(MyFunctional func, String input) {
return func.func(input);
}

public static void main(String[] args) {
String reverseMessage = execute((input) -> new StringBuilder(input).reverse().toString(), "Java");

System.out.println(reverseMessage);
}

}

Generic функциональные интерфейсы

А как быть с Generic'ами спросите вы? Сами лямбда выражения не могут быть Generic, но их функциональный интерфейс может. Давайте создадим Generic функциональный интерфейс

interface MyFunctional<T> {
T func(T input);

Здесь мы создали Generic функциональный интерфейс. Думаю что всем все понятно. Если здесь вы видите что-то новое, советую вам прочитать про обобщения (Generics).

Давайте же теперь используем этот интерфейс в лямбда выражении.

public class Main {

private static <T> T execute(MyFunctional<T> func, T input) {
return func.func(input);
}

public static void main(String[] args) {
MyFunctional<String> myFunctional = (input) -> {
return Arrays.stream(input.split(" "))
.filter(s -> s.length() > 3)
.map(s -> new StringBuilder(s).reverse().toString())
.collect(Collectors.joining(" "));
};

String result = execute(myFunctional, "Java the best programming language");
System.out.println(result);
}

}

Здесь мы создаем generic функциональный интерфейс с помощью лямбды и сохраняем его в переменную, я сохранил его в переменную потому что так читать код легче. Кстати, здесь вы можете видеть блочную лямбду. Блочная используется тогда, когда тело лямбда выражения не может уместиться в одну строку. Обратите внимание на то, что линейная лямбда от блочной отличается только тем, что у блочную присутствуют скобки {}. А также блочная лямбда должна возвращать явно значение (оператор return). НО - это не обязательно, исключением являются void функциональные интерфейсы которые ничего не возвращают. 

Лямбда из нашего примера принимает на вход строку и все слова в строке у которых длина больше 3 символов переворачивает. Мы этот дженерик интерфейс можем также использовать и с любым другим типом, например Integer и реализовать другое тело выражения. При этом мы не плодим новый интерфейс, а используем обобщенный.

Доступ к переменным контекста в лямбда выражениях

Лямбда выражения имеют доступ к переменным окружающих их контекста. Они могут получить следующие переменные.

  1. Переменные экземпляра
  2. Локальные переменные

Но при этом, лямбда не может изменить значение локальных переменных.

public class Main {

private static boolean isCallee = false;

private static <T> T execute(MyFunctional<T> func, T input) {
return func.func(input);
}

public static void main(String[] args) {
String result = execute((input) -> {
isCallee = true;

// ... some code
}, "Java the best programming language");

if (isCallee) {
System.out.println(result);
}
}
}

Здесь мы создали переменную статическую в классе в котором вызывается лямбда. Из лямбды эта переменная может меняться и все будет отлично. Ошибки компиляции не произойдет. Так же, если бы это был не статический класс, у лямбды была бы ссылка на контекст экземпляра с помощью this.

Рассмотрим следующий пример. 

public class Main {

private static <T> T execute(MyFunctional<T> func, T input) {
return func.func(input);
}

public static void main(String[] args) {
boolean isCallee = false;

String result = execute((input) -> {
isCallee = true;

// ... some code
}, "Java the best programming language");

if (isCallee) {
System.out.println(result);
}
}

}

Этот код не скомпилируется. Потому что лямбда пытается изменить локальную переменную. Этого делать нельзя чтобы не привести к не очевидным ошибкам. 

Итак, запомним! лямбда НЕ может менять локальные переменные, но МОЖЕТ изменять значение переменных экземпляра.

Лямбда выражения это классная вещь, которая пришла к нам с появлением Java SE 8, и способная облегчить написание кода и его рефакторинг. 

Но все что мы делали в этом уроке - это изобретение велосипедов, так как в Java есть достаточно много встроенных функциональных интерфейсов. Смотрите их в пакете java.util.functional

Если у Вас остались вопросы, то задавайте их в комментариях.

Java
27.10.2016
1 ответ
авторизуйтесь чтобы ответить