Если нужен короткий ответ: lambda-выражение это компактная запись безымянной функции, которую передают в метод как обычный аргумент. Записывают ее как
(аргументы) -> тело, работает она с Java 8 и подставляется всюду, где ожидается функциональный интерфейс, то есть интерфейс ровно с одним абстрактным методом:
Runnable,
Comparator,
Predicate. Компилятор сам понимает, какой именно метод вы реализуете, поэтому семь строк анонимного класса сжимаются до одной.
Кратко
- Lambda-выражение это функция без имени: параметры, стрелка
-> и тело. Модификатора доступа, типа возвращаемого значения и имени у нее нет. - Присвоить лямбду можно только переменной функционального интерфейса, то есть интерфейса ровно с одним абстрактным методом.
- Если параметр один и его тип выводится из контекста, круглые скобки не нужны. Если тело это одно выражение, не нужны ни фигурные скобки, ни
return. - Аннотация
@FunctionalInterface ничего не создает: она только просит компилятор проверить, что абстрактный метод в интерфейсе действительно один. - Внутри лямбды
this указывает на объект внешнего класса, а не на саму лямбду. У анонимного класса наоборот, и это главное отличие между ними. - Отдельный class-файл лямбда не создает: компилятор превращает ее тело в приватный метод и связывает вызов через
invokedynamic.
Java изначально полностью объектно-ориентированный язык. За исключением примитивных типов, все в Java это объекты. Даже массивы являются объектами. Экземпляры каждого класса тоже объекты. Не существует ни единой возможности определить отдельно (вне класса,
прим. перев.) какую-нибудь функцию. И нет никакой возможности передать метод как аргумент или вернуть тело метода как результат другого метода. Все так. Но так было до Java 8.
![Логотип Java: дымящаяся чашка кофе с оранжевым паром]()
Со времен старого доброго Swing, надо было писать анонимные классы, когда нужно было передать некую функциональность в какой-нибудь метод. Например, так выглядело добавление обработчика событий:
someObject.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
}
});
Здесь мы хотим добавить некоторый код в слушатель событий от мыши. Мы определили анонимный класс
MouseAdapter и сразу создали объект из него. Таким способом мы передали дополнительную функциональность в метод
addMouseListener.
Короче говоря, не так-то просто передать простой метод (функциональность) в Java через аргументы. Это ограничение вынудило разработчиков Java 8 добавить в спецификацию языка такую возможность как Lambda-выражения.
Зачем яве Lambda-выражения?
С самого начала, язык Java особо не развивался, если не считать такие вещи как аннотации (Annotations), дженерики (Generics) и пр. В первую очередь, Java всегда оставался объектно-ориентированным. После работы с функциональными языками, такими как JavaScript, можно понять насколько Java строго объектно-ориентирован и строго типизирован. Функции в Java не нужны. Сами по себе их нельзя встретить в мире Java.
В функциональных языках программирования на первый план выходят функции. Они существуют сами по себе. Можно присваивать их переменным и передавать через аргументы другим функциям. JavaScript один из лучших примеров функциональных языков программирования. На просторах Интернета можно найти хорошие статьи, в которых детально описаны преимущества JavaScript как функционального языка. Функциональные языки имеют в своем арсенале такие мощные инструменты как замыкания (Closure), которые обеспечивают ряд преимуществ над традиционными способами написания приложений. Замыкание это функция с привязанной к ней средой: таблицей, хранящей ссылки на все нелокальные переменные функции. В Java замыкания можно имитировать через Lambda-выражения. Безусловно между замыканиями и Lambda-выражениями есть отличия, и немалые, но лямбда-выражения являются хорошей альтернативой замыканиям.
В своем саркастичном и забавном блоге, Стив Иег (Steve Yegge) описывает, насколько мир Java строго завязан на имена существительные (сущности, объекты,
прим. перев.). Если вы не читали его пост
Execution in the Kingdom of Nouns, рекомендую. Он забавно и интересно описывает точную причину того, почему в Java добавили Lambda-выражения.
Lambda-выражения привносят в Java функциональное звено, которого так давно не хватало. Lambda-выражения вносят в язык функциональность наравне с объектами. Хотя это и не на 100% верно, можно видеть, что Lambda-выражения не являясь замыканиями предоставляют схожие возможности. В функциональном языке lambda-выражения это функции; но в Java lambda-выражения представляются объектами и должны быть связаны с конкретным объектным типом, который называется функциональный интерфейс. Далее мы рассмотрим, что он из себя представляет.
В статье Марио Фаско (Mario Fusco) “Зачем в Java нужны Lambda-выражения” (“Why we need Lambda Expression in Java”) подробно описано, зачем всем современным языкам нужны возможности замыканий.
Введение в Lambda-выражения
Lambda-выражения это анонимные функции (может и не 100% верное определение для Java, но зато привносит некоторую ясность). Проще говоря, это метод без объявления, т.е. без модификатора доступа, возвращаемого значения и имени.
Короче говоря, они позволяют написать метод и сразу же использовать его. Особенно полезно в случае однократного вызова метода, т.к. сокращает время на объявление и написание метода без необходимости создавать класс.
Lambda-выражения в Java обычно имеют следующий синтаксис
(аргументы) -> (тело). Например:
(арг1, арг2...) -> { тело }
(тип1 арг1, тип2 арг2...) -> { тело }
Далее идет несколько примеров настоящих Lambda-выражений:
(int a, int b) -> { return a + b; }
() -> System.out.println("Hello World");
(String s) -> { System.out.println(s); }
() -> 42
() -> { return 3.1415; };
Структура Lambda-выражений
Давайте изучим структуру lambda-выражений:
- Lambda-выражения могут иметь от 0 и более входных параметров.
- Тип параметров можно указывать явно либо может быть получен из контекста. Например (
int a) можно записать и так (a)
- Параметры заключаются в круглые скобки и разделяются запятыми. Например (
a, b) или (int a, int b) или (String a, int b, float c)
- Если параметров нет, то нужно использовать пустые круглые скобки. Например
() -> 42
- Когда параметр один, если тип не указывается явно, скобки можно опустить. Пример:
a -> a*a
- Тело Lambda-выражения может содержать от 0 и более выражений.
- Если тело состоит из одного оператора, его можно не заключать в фигурные скобки, а возвращаемое значение можно указывать без ключевого слова
return.
- В противном случае фигурные скобки обязательны (блок кода), а в конце надо указывать возвращаемое значение с использованием ключевого слова
return (в противном случае типом возвращаемого значения будет void).
- С Java 11 вместо имени типа можно писать
var: (var a, var b) -> a + b. Правило одно: либо var стоит у всех параметров, либо ни у одного, смешивать var с явными типами нельзя.
Что такое функциональный интерфейс
В Java маркерные интерфейсы (Marker interface) это интерфейсы без объявления методов и полей. Другими словами, маркерные интерфейсы это пустые интерфейсы. Точно так же функциональные интерфейсы (Functional Interface) это интерфейсы только с одним абстрактным методом, объявленным в нем.
java.lang.Runnable это пример функционального интерфейса. В нем объявлен только один метод
void run(). Также есть интерфейс
ActionListener, он тоже функциональный. Раньше нам приходилось использовать анонимные классы для создания объектов, реализующих функциональный интерфейс. С Lambda-выражениями, все стало проще.
Каждое lambda-выражение может быть неявно привязано к какому-нибудь функциональному интерфейсу. Например, можно создать ссылку на
Runnable интерфейс, как показано в следующем примере:
Runnable r = () -> System.out.println("hello world");
Подобное преобразование всегда осуществляется неявно, когда мы не указываем функциональный интерфейс:
new Thread(
() -> System.out.println("hello world")
).start();
В примере выше, компилятор автоматически создает lambda-выражение как реализацию
Runnable интерфейса из конструктора класса
Thread:
public Thread(Runnable r) { }.
Приведу несколько примеров lambda-выражений и соответствующих функциональных интерфейсов:
Consumer<Integer> c = (Integer x) -> { System.out.println(x); };
BiConsumer<Integer, String> b = (Integer x, String y) -> System.out.println(x + " : " + y);
Predicate<String> p = (String s) -> { return s == null; };
Аннотация
@FunctionalInterface, добавленная в Java 8 согласно Java Language Specification, проверяет, является ли объявляемый интерфейс функциональным. Кроме того, в Java 8 включен
ряд готовых функциональных интерфейсов для использования с Lambda-выражениями.
@FunctionalInterface выдаст ошибку компиляции, если объявляемый интерфейс не будет функциональным.
Далее приводится пример определения функционального интерфейса:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
Как следует из определения, функциональный интерфейс может иметь только один абстрактный метод. Ограничение касается именно абстрактных методов. У
default- и
static-методов есть тело, поэтому в счет они не идут, и таких методов в интерфейсе может быть сколько угодно. Не идут в счет и абстрактные методы, которые переопределяют публичные методы
Object: в
Comparator объявлен
equals(), и интерфейс все равно остается функциональным. А вот если попытаться добавить еще один абстрактный метод, то вылезет ошибка компиляции. Пример:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
public void doSomeMoreWork();
}
Error
Unexpected @FunctionalInterface annotation
@FunctionalInterface ^ WorkerInterface is not a functional interface multiple
non-overriding abstract methods found in interface WorkerInterface 1 error
После определения функционального интерфейса, мы можем его использовать и получать все преимущества Lambda-выражений. Пример:
@FunctionalInterface
public interface WorkerInterface {
public void doSomeWork();
}
public class WorkerInterfaceTest {
public static void execute(WorkerInterface worker) {
worker.doSomeWork();
}
public static void main(String [] args) {
execute(new WorkerInterface() {
@Override
public void doSomeWork() {
System.out.println("Worker вызван через анонимный класс");
}
});
execute( () -> System.out.println("Worker вызван через Lambda") );
}
}
Вывод:
Worker вызван через анонимный класс
Worker вызван через Lambda
Здесь мы определили свой собственный функциональный интерфейс и воспользовались lambda-выражением. Метод
execute() способен принимать lambda-выражения в качестве аргумента.
Примеры Lambda-выражений
Лучший способ вникнуть в Lambda-выражения, это рассмотреть несколько примеров.
Запуск потока: Thread и Runnable
Поток
Thread можно проинициализировать двумя способами:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello from thread");
}
}).start();
new Thread(
() -> System.out.println("Hello from thread")
).start();
Обработчик события: ActionListener
Управление событиями в Java 8 также можно осуществлять через Lambda-выражения. Далее представлены два способа добавления обработчика события
ActionListener в компонент пользовательского интерфейса:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Кнопка нажата. Старый способ!");
}
});
button.addActionListener( (e) -> {
System.out.println("Кнопка нажата. Lambda!");
});
Перебор списка: forEach и ссылка на метод
Простой пример вывода всех элементов заданного массива. Заметьте, что есть более одного способа использования lambda-выражения. Ниже мы создаем lambda-выражение обычным способом, используя синтаксис стрелки, а также ссылку на метод
System.out::println: это короткая запись для лямбды, тело которой сводится к вызову одного готового метода:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) {
System.out.println(n);
}
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));
list.forEach(System.out::println);
Отбор по условию: Predicate
В следующем примере мы используем функциональный интерфейс
Predicate для создания теста и печати элементов, прошедших этот тест. Таким способом вы можете помещать логику в lambda-выражения и делать что-либо на ее основе.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
public class Main {
public static void main(String [] a) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
System.out.print("Выводит все числа: ");
evaluate(list, (n)->true);
System.out.print("Не выводит ни одного числа: ");
evaluate(list, (n)->false);
System.out.print("Вывод четных чисел: ");
evaluate(list, (n)-> n%2 == 0 );
System.out.print("Вывод нечетных чисел: ");
evaluate(list, (n)-> n%2 == 1 );
System.out.print("Вывод чисел больше 5: ");
evaluate(list, (n)-> n > 5 );
}
public static void evaluate(List<Integer> list, Predicate<Integer> predicate) {
for(Integer n: list) {
if(predicate.test(n)) {
System.out.print(n + " ");
}
}
System.out.println();
}
}
Вывод:
Выводит все числа: 1 2 3 4 5 6 7
Не выводит ни одного числа:
Вывод четных чисел: 2 4 6
Вывод нечетных чисел: 1 3 5 7
Вывод чисел больше 5: 6 7
Квадрат каждого элемента: stream и map
Поколдовав над Lambda-выражениями, можно вывести квадрат каждого элемента списка. Заметьте, что мы используем метод
stream(), чтобы преобразовать обычный список в поток. Java 8 предоставляет шикарный класс
Stream (
java.util.stream.Stream). Он содержит тонны полезных методов, с которыми можно использовать lambda-выражения, и разбору этого класса посвящена отдельная статья про
Stream API. Мы передаем lambda-выражение
x -> x*x в метод
map(), который применяет его ко всем элементам в потоке. После чего мы используем
forEach для печати всех элементов списка.
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
for(Integer n : list) {
int x = n * n;
System.out.println(x);
}
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
list.stream().map((x) -> x*x).forEach(System.out::println);
Сумма квадратов: reduce
Дан список, нужно вывести сумму квадратов всех элементов списка. Lambda-выражения позволяют достигнуть этого написанием всего одной строки кода. В этом примере применен метод свертки (редукции)
reduce(). Мы используем метод
map() для возведения в квадрат каждого элемента, а потом применяем метод
reduce() для свертки всех элементов в одно число.
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = 0;
for(Integer n : list) {
int x = n * n;
sum = sum + x;
}
System.out.println(sum);
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7);
int sum = list.stream().map(x -> x*x).reduce((x,y) -> x + y).get();
System.out.println(sum);
Отличие Lambda-выражений от анонимных классов
Главное отличие состоит в использовании ключевого слова
this. Для
анонимных классов ключевое слово ‘
this’ обозначает объект анонимного класса, в то время как в lambda-выражении ‘
this’ обозначает объект класса, в котором lambda-выражение используется.
Другое их отличие заключается в способе компиляции. Java компилирует lambda-выражения с преобразованием их в
private-методы класса. При этом используется инструкция
invokedynamic, появившаяся в Java 7 для динамической привязки метода. Тал Вайс (Tal Weiss) описал в своем блоге, как Java компилирует lambda-выражения в байт-код. Если свести отличия в таблицу, получится так.
| Признак |
Анонимный класс |
Lambda-выражение |
| Что можно реализовать |
любой интерфейс или абстрактный класс, абстрактных методов может быть сколько угодно |
только функциональный интерфейс, ровно один абстрактный метод |
Ключевое слово this |
объект самого анонимного класса |
объект внешнего класса, в котором написана лямбда |
| Свои поля и состояние |
можно объявить поля и хранить в них состояние |
своих полей нет, есть только параметры и локальные переменные тела |
| Результат компиляции |
отдельный class-файл вида Outer$1.class |
приватный метод класса, вызов связывается через invokedynamic |
| Объем кода на один метод |
имя типа, @Override и полная сигнатура метода |
параметры, стрелка и тело |
Заключение
Марк Рейнхолд (Mark Reinhold - Oracle’s Chief Architect), назвал Lambda-выражения самым значительным изменением в модели программирования, которое когда-либо происходило, даже более значительным, чем дженерики (generics). Должно быть он прав, т.к. они дают Java программистам возможности функциональных языков программирования, которых так давно все ждали. Наряду с такими новшествами как методы виртуального расширения (Virtual extension methods), Lambda-выражения позволяют писать очень качественный код.
Я надеюсь, что эта статья позволила вам взглянуть под капот Java 8. Удачи :)
Вопросы и ответы
Чем lambda-выражение отличается от анонимного класса?
Внутри анонимного класса
this указывает на сам этот класс, внутри лямбды на внешний объект. Анонимный класс может реализовать любой интерфейс с любым числом методов и завести собственные поля, лямбде доступен только функциональный интерфейс, и полей у нее нет. Компилятор поступает с ними тоже по-разному: из анонимного класса получается отдельный class-файл, из лямбды приватный метод и вызов через
invokedynamic.
В какой интерфейс можно передать lambda-выражение?
Только в функциональный, то есть в такой, где ровно один абстрактный метод. Это
Runnable,
Comparator,
Callable,
Predicate,
Consumer,
Function и любой ваш собственный интерфейс с одним методом. Если абстрактных методов два, компилятор лямбду не примет.
Обязательно ли ставить аннотацию @FunctionalInterface?
Нет. Компилятор считает функциональным любой интерфейс с одним абстрактным методом независимо от аннотации.
@FunctionalInterface нужна для страховки: если кто-то потом добавит в интерфейс второй абстрактный метод, сборка упадет сразу на самом интерфейсе, а не в том месте, где лямбду пытаются подставить.
Может ли функциональный интерфейс содержать больше одного метода?
Да, если лишние методы не абстрактные. У
default- и
static-методов есть тело, поэтому они не считаются. Не считаются и абстрактные методы, которые переопределяют публичные методы
Object, например
equals() в
Comparator. Ограничение ровно одно: метод без реализации должен быть один.
Почему переменную из внешнего метода нельзя менять внутри лямбды?
Лямбда захватывает не саму переменную, а ее значение на момент создания. Поэтому Java требует, чтобы локальная переменная была final или effectively final, то есть чтобы ей нигде больше не присваивали новое значение. Если значение нужно менять, заведите поле класса или массив из одного элемента: их содержимое лямбда меняет свободно.
Когда вместо лямбды лучше писать ссылку на метод?
Когда тело лямбды это только вызов уже существующего метода с теми же аргументами. Вместо
n -> System.out.println(n) пишут
System.out::println, вместо
s -> s.length() пишут
String::length. Смысл тот же, а читается короче. Если внутри есть хоть какая-то дополнительная логика, ссылка на метод не подойдет.
Источник:
Oracle, The Java Tutorials: Lambda Expressions.
Читайте также