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

Определение

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

Comparator<String> byLength =
        (left, right) -> Integer.compare(left.length(), right.length());

Левая часть задаёт параметры, правая — одно выражение или блок:

Predicate<String> shortText = text -> text.length() < 20;
 
Function<String, String> normalize = text -> {
    String trimmed = text.trim();
    return trimmed.toLowerCase(Locale.ROOT);
};

Лямбда не выполняется при объявлении. Она создаёт значение поведения; вызов происходит через SAM (test, apply, accept и т. п.).

Target typing

Лямбда получает тип из контекста:

Predicate<String> first = value -> value.isEmpty();
Function<String, Boolean> second = value -> value.isEmpty();

Без целевого интерфейса выражение вроде value -> value.isEmpty() не имеет самостоятельного типа. Перегруженные методы с одинаковой формой SAM могут потребовать явного cast или, лучше, другого имени API.

Захват переменных

Лямбда может читать локальные переменные, если они final или effectively final:

int limit = 10;
Predicate<String> fits = text -> text.length() <= limit;

После захвата limit нельзя переassignить. Ограничение предотвращает иллюзию совместной изменяемой локальной переменной, время жизни которой закончилось вместе с методом.

Поля объекта можно менять, но это уже общий mutable-state и требует обычных правил потокобезопасности.

this и область видимости

Лямбда не создаёт нового значения this: оно относится к окружающему объекту. В анонимном классе this относится к экземпляру самого анонимного класса.

Лямбда также не позволяет заново объявить локальное имя из внешней области:

int count = 1;
// Consumer<String> action = count -> {}; // конфликт имени

Ссылки на методы

Четыре формы:

String::valueOf          // static-метод
text::startsWith         // метод конкретного объекта
String::length           // instance-метод произвольного объекта типа
ArrayList<String>::new   // конструктор

Ссылка должна соответствовать функциональному дескриптору:

Function<String, Integer> length = String::length;
Supplier<List<String>> factory = ArrayList::new;

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

Checked-исключения

Тело может выбрасывать checked-исключение только если SAM это разрешает:

@FunctionalInterface
interface Parser<T> {
    T parse(Path path) throws IOException;
}
 
Parser<String> parser = Files::readString;

Стандартный Function<Path,String> такого throws не имеет.

Лямбда и identity

Не следует полагаться на идентичность объекта лямбды, его конкретный класс или сериализуемость. JVM свободна переиспользовать экземпляры и реализовывать их различными способами. Контракт — только поведение функционального интерфейса.

Ловушки

  • Считать лямбду самостоятельным типом.
  • Менять захваченное состояние без синхронизации.
  • Использовать массив/обёртку как обход effectively final и прятать mutable-счётчик.
  • Заворачивать checked-ошибку в безымянный RuntimeException.
  • Выбирать ссылку на перегруженный метод, если лямбда яснее.
  • Выполнять I/O или бизнес-эффекты внутри map без явного контракта.

Вопросы на собеседовании

  1. Во что типизируется лямбда?
  2. Что означает effectively final?
  3. Чем this в лямбде отличается от this в анонимном классе?
  4. Какие четыре формы ссылок на методы существуют?
  5. Может ли лямбда выбросить checked-исключение?
  6. Можно ли полагаться на класс или идентичность объекта лямбды?

Связанные заметки

Функциональные интерфейсы · ФИ стандартной библиотеки Java · Функциональное программирование · Stream API · Вложенные классы