Лямбда-выражения в Java
Краткая встроенная реализация функциональных интерфейсов в Java с помощью лямбда-выражений: (params) -> body.
Лямбда-выражение — это лаконичный синтаксис, добавленный в Java 8 для «экземпляра интерфейса, имеющего ровно один абстрактный метод». До появления лямбд это записывалось через анонимный класс. Теперь достаточно указать список параметров, стрелку и тело:
Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();Здесь нет нового вида значений — r, byLen и length по-прежнему являются ссылками на объекты, и во время выполнения каждый из них содержит экземпляр класса, реализующего интерфейс слева. Новизна в том, что код, создающий такой экземпляр, теперь достаточно короткий, чтобы разместиться прямо в точке вызова — именно это открывает все остальные функциональные идиомы: предикаты для фильтрации, построители компараторов, обработчики событий, пайплайны стримов.
Синтаксические формы
Лямбда состоит из трёх частей: список параметров, стрелка -> и тело. Каждая часть допускает сокращённую запись:
// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");
// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length(); // same thing
// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();
// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;
// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;
// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
};Три правила связывают всё вместе:
- Типы параметров обычно выводятся из целевого типа (интерфейса, объявленного в точке вызова). Указывайте их явно только тогда, когда компилятор не может их определить или когда это повышает читаемость.
- Тело-выражение возвращает своё значение неявно. Без
returnи без точки с запятой. Выражение и есть результат. - Тело-блок требует
return, когда метод интерфейса имеет возвращаемый тип. Забытыйreturn— это ошибка компиляции, а не тихийnull.
Целевая типизация — где могут появляться лямбды
Лямбда не имеет собственного типа. Компилятор определяет её тип из цели — контекста, в котором она используется:
Runnable r1 = () -> doWork(); // target: Runnable
Callable<Integer> c1 = () -> 42; // target: Callable<Integer>
Supplier<Integer> s1 = () -> 42; // target: Supplier<Integer>() -> 42 — это один и тот же исходный код во всех трёх случаях, но он компилируется в три разных экземпляра интерфейса. Именно поэтому лямбду нельзя присвоить напрямую переменной типа Object — Object o = () -> 42; неоднозначно, и компилятор откажет. Для устранения неоднозначности используйте приведение типов: Object o = (Supplier<Integer>) () -> 42;.
Наиболее частые цели:
- Параметр метода с типом функционального интерфейса:
list.removeIf(s -> s.isEmpty()). - Поле или локальная переменная с типом функционального интерфейса:
Predicate<String> empty = String::isEmpty;. - Возвращаемый тип:
public Supplier<Date> now() { return Date::new; }.
Без цели лямбда существовать не может. var f = s -> s.length(); не компилируется — var не может вывести целевой тип.
Захват переменных: «эффективно финальные»
Лямбда может читать локальные переменные из охватывающего метода, но только если эти переменные эффективно финальны — то есть никогда не переприсваиваются после первоначального присвоения:
int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier; // OK — `multiplier` never reassigned
multiplier = 4; // <-- this line would make the lambda not compileЭто то же правило, которое всегда действовало для анонимных внутренних классов, и причина та же: лямбда может пережить метод, в котором она определена (её можно сохранить в поле или передать в другой поток), а Java не поддерживает замыкания, захватывающие переменную — захватывается значение в момент создания. Разрешение переприсваивания создавало бы запутанную иллюзию.
Поля — другое дело. Лямбда может свободно читать и изменять поля экземпляра и статические поля:
class Counter {
private int n = 0;
Runnable inc = () -> n++; // legal — `n` is a field, not a local
}Это частый источник ошибок в коде со стримами — лямбда, изменяющая общее поле, выглядит безобидно, но вступает в гонку с собой, когда стрим становится параллельным. Чистые лямбды безопаснее.
this, return и break внутри лямбды
Лямбда не создаёт нового контекста для this. Внутри лямбды this ссылается на охватывающий экземпляр — так же, как и в окружающем коде:
class Greeter {
String prefix = "Hello, ";
Function<String, String> greet = name -> this.prefix + name; // `this` is the Greeter
}Это одно из главных практических отличий от анонимных классов, где this ссылалось на сам анонимный экземпляр.
return внутри лямбды возвращает управление из лямбды, а не из охватывающего метода. break и continue в лямбде не работают — они принадлежат циклу, которому адресованы, а тело лямбды не является частью окружающего цикла.
Лямбда против анонимного класса — когда что использовать
Для функциональных интерфейсов лямбды почти всегда короче и понятнее. Они генерируют немного другой байткод (invokedynamic) и не создают отдельный файл класса на каждое место использования, поэтому, как правило, легче в рантайме.
Используйте анонимный класс, когда:
- Интерфейс имеет более одного абстрактного метода (он не является функциональным).
- Вам нужно локальное поле метода (
int seen = 0;, доступное между вызовами). - Вам нужно, чтобы
thisссылалось на создаваемый экземпляр, а не на охватывающий. - Вам нужно переопределить метод по умолчанию для специализации его поведения.
Во всех остальных случаях лямбда предпочтительнее.
Развёрнутый пример: захват переменных, целевая типизация, четыре точки вызова
Программа ниже демонстрирует четыре наиболее распространённых места, где появляется лямбда, — forEach коллекции, removeIf, сортировка и фильтр стрима, — а также правила захвата и целевую типизацию.
Что можно вынести из запуска:
() -> \"hi\"сработало и какCallable<String>, и какSupplier<String>— один и тот же исходный код, разные целевые типы, разные экземпляры интерфейса. Именно поэтому у лямбды нет типа до тех пор, пока его не предоставит контекст.times = n -> n * factorзахватилfactorпо значению. Компилятор принял это, потому чтоfactorни разу не переприсваивался. Раскомментированиеfactor = 11сделало быfactorне-эффективно-финальной переменной и нарушило бы компиляцию лямбды.forEach,removeIfиsortпринимают разные функциональные интерфейсы (Consumer,Predicate,Comparator), а форма лямбды — количество параметров, наличие возвращаемого значения — соответствует единственному абстрактному методу каждого интерфейса. Сопоставление выполняет компилятор с помощью целевой типизации.- Лямбда-блок
describeпотребовала явных операторовreturn, поскольку её цель (Function<Integer, String>) имеет ненулевой возвращаемый тип. Лямбды с телом-выражением выше возвращали своё выражение неявно.
Что дальше
Вы знаете синтаксис и правила захвата. Следующий вопрос: какому именно интерфейсу компилируется лямбда? Функциональные интерфейсы Java знакомит с правилом единственного абстрактного метода (SAM), аннотацией @FunctionalInterface и написанием собственного функционального интерфейса для случаев, не охваченных стандартной библиотекой.