W3docs

Лямбда-выражения в 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";
};

Три правила связывают всё вместе:

  1. Типы параметров обычно выводятся из целевого типа (интерфейса, объявленного в точке вызова). Указывайте их явно только тогда, когда компилятор не может их определить или когда это повышает читаемость.
  2. Тело-выражение возвращает своё значение неявно. Без return и без точки с запятой. Выражение и есть результат.
  3. Тело-блок требует return, когда метод интерфейса имеет возвращаемый тип. Забытый return — это ошибка компиляции, а не тихий null.

Целевая типизация — где могут появляться лямбды

Лямбда не имеет собственного типа. Компилятор определяет её тип из цели — контекста, в котором она используется:

Runnable           r1 = () -> doWork();          // target: Runnable
Callable<Integer>  c1 = () -> 42;                // target: Callable<Integer>
Supplier<Integer>  s1 = () -> 42;                // target: Supplier<Integer>

() -> 42 — это один и тот же исходный код во всех трёх случаях, но он компилируется в три разных экземпляра интерфейса. Именно поэтому лямбду нельзя присвоить напрямую переменной типа ObjectObject 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, сортировка и фильтр стрима, — а также правила захвата и целевую типизацию.

java— editable, runs on the server

Что можно вынести из запуска:

  • () -> \"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 и написанием собственного функционального интерфейса для случаев, не охваченных стандартной библиотекой.

Практика

Практика
Какое из этих лямбда-выражений компилятор Java отклонит?
Какое из этих лямбда-выражений компилятор Java отклонит?
Was this page helpful?