Поведение, которое можно передавать
Лямбда-выражение — это компактный блок кода, который можно передать в метод, сохранить в переменной или вернуть, точно так же, как число или строку. До появления лямбд передача поведения означала написание целого класса (или многословного анонимного класса) только ради того, чтобы обернуть один метод. Лямбда сводит это к сути: к параметрам и телу.
Лямбда всегда реализует функциональный интерфейс — интерфейс ровно с одним абстрактным методом (вы встречали их в конце страницы про интерфейсы). Компилятор по контексту определяет, какой интерфейс вы имеете в виду, и сопоставляет вашу лямбду с этим единственным методом.
x -> x * 2 — это полная реализация apply. Никакого new, никакого тела класса, никакого имени метода — всё это берёт на себя интерфейс.
Синтаксис со стрелкой
Любая лямбда имеет форму параметры -> тело. Части гибко меняются в зависимости от того, что вам нужно:
() -> 42 // без параметров
x -> x + 1 // один параметр, скобки необязательны
(x, y) -> x + y // два и более параметра требуют скобок
(int x, int y) -> x + y // типы необязательны - обычно выводятся
x -> { // блочное тело требует фигурных скобок и return
int doubled = x * 2;
return doubled + 1;
}
Тело из одного выражения (x -> x + 1) неявно возвращает своё значение — без ключевого слова return. Как только вы используете фигурные скобки, вы пишете обычный блок и обязаны указывать return явно, если метод что-то возвращает. Частая ошибка — смешивать эти два варианта: x -> { x + 1 } не компилируется, потому что блоку нужна инструкция (return x + 1;).
Лямбды заменяют анонимные классы
Нагляднее всего польза от лямбды видна в сравнении «до и после». Сортировка с собственным Comparator раньше выглядела так:
// до - анонимный класс
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
То же самое в виде лямбды — это одна читаемая строка:
Comparator — это функциональный интерфейс (его единственный абстрактный метод — compare), поэтому лямбда подставляется напрямую. Все церемонии — new, класс, сигнатура метода — исчезают, оставляя лишь логику сравнения.
Набор инструментов java.util.function
Объявлять собственный функциональный интерфейс приходится редко. Пакет java.util.function содержит распространённые формы, и почти все библиотечные API Java их принимают:
Function<T, R>— принимаетT, возвращаетR(apply)Predicate<T>— принимаетT, возвращаетboolean(test)Supplier<T>— ничего не принимает, возвращаетT(get)Consumer<T>— принимаетT, ничего не возвращает (accept)
Это обобщённые (generic) интерфейсы: Function<String, Integer> повторно использует дженерики, которые вы видели на предыдущей странице, чтобы оставаться типобезопасным. Выбирайте тот, чья форма соответствует тому, что вашему коду нужно принимать и производить.
Ссылки на методы
Когда лямбда не делает ничего, кроме вызова одного уже существующего метода, её можно заменить ссылкой на метод с помощью ::. Это то же самое значение, записанное более прямо:
Ссылки на методы бывают разных видов: String::toUpperCase (метод экземпляра, вызываемый на каждом аргументе), Math::max (статический метод), System.out::println (метод конкретного объекта) и ArrayList::new (конструктор). Прибегайте к ним только тогда, когда они читаются ясно: если приходится задумываться, какая форма применяется, обычная лямбда вполне подойдёт.
Захват переменных
Лямбда может использовать локальные переменные окружающего метода, но только если они final или фактически final — присвоены один раз и больше не изменяются. Java захватывает значение в момент создания лямбды, поэтому переменная, которая могла бы измениться позже, была бы неоднозначной.
Если вы переприсвоите factor где-либо, компилятор отклонит лямбду с сообщением «variable used in lambda expression should be final or effectively final». Когда вам действительно нужно изменяемое разделяемое состояние, захватывайте вместо этого объект — поле, элемент массива или AtomicInteger, — потому что ссылка остаётся неизменной, даже если её содержимое меняется. Обратите внимание: лямбды, в отличие от анонимных классов, не создают собственную область видимости: this внутри лямбды ссылается на охватывающий экземпляр, а не на саму лямбду.
Далее: Streams
Лямбды — это строительный блок, а не конечная цель. Их настоящая отдача проявляется с API Streams, где вы выстраиваете цепочки операций вроде filter, map и reduce — каждая принимает лямбду, — чтобы выразить преобразования данных как читаемый конвейер вместо клубка циклов. Это тема следующей страницы.
Часто задаваемые вопросы
Что такое лямбда-выражение в Java?
Лямбда — это краткий способ записать экземпляр функционального интерфейса, то есть интерфейса с единственным абстрактным методом. Вместо целого анонимного класса вы пишете параметры -> тело. Компилятор сопоставляет лямбду с единственным методом интерфейса, поэтому Runnable r = () -> System.out.println("hi"); — это полноценный Runnable. Лямбды позволяют передавать поведение так же, как данные.
В чём разница между лямбдой и ссылкой на метод в Java?
Оба варианта порождают экземпляр функционального интерфейса. У лямбды есть явные параметры и тело (s -> s.toUpperCase()), а ссылка на метод — это сокращённая запись лямбды, которая просто вызывает один уже существующий метод (String::toUpperCase). Используйте ссылку на метод, когда лямбда не делает ничего, кроме передачи своих аргументов одному именованному методу: так короче и читается лучше.
Почему переменные, используемые в лямбде Java, должны быть final или фактически final?
Лямбда может захватывать локальные переменные охватывающего метода, но только если они никогда не меняются после присваивания — именно это значит «фактически final». Java захватывает значение, а не живую ссылку на переменную, поэтому разрешить переприсваивание было бы неоднозначно и небезопасно при работе с потоками. Если вам нужно изменяемое разделяемое состояние, используйте вместо этого поле или обёртку — массив либо AtomicInteger.