Лямбда-выражения в Java 8: основы и примеры. Практические Задания
Самая частая поломка в задаче 2 из этого списка выглядит так: два предиката переставлены местами. Вызов notEmpty.and(notNull).test(null) падает с NullPointerException, хотя проверка на null в коде есть. Причина — and() вычисляет условия слева направо с коротким замыканием, поэтому первым отрабатывает !s.isEmpty() на null. Правильный порядок — notNull.and(notEmpty). Такие детали не видны в теории и всплывают только тогда, когда пишешь код сам.
Лямбда-выражение в Java — это компактная запись реализации функционального интерфейса (интерфейса ровно с одним абстрактным методом) без создания анонимного класса. Лямбды и ссылки на методы появились в Java 8 и с тех пор остаются базой для Stream API и всего функционального стиля в языке. Ниже — 7 практических заданий: от простейшего () -> ... до композиции предикатов и ссылок на статический метод. К большинству задач есть видеоразбор решения.
Что повторить перед решением
Каждая задача опирается на один встроенный функциональный интерфейс из пакета java.util.function. Если забыли сигнатуру — откройте соответствующий урок в отдельной вкладке, но решение пишите сами.
| Задача | Интерфейс | Абстрактный метод | Что отрабатываем |
|---|---|---|---|
| 1, 7 | Свой интерфейс Printable | void print() | Лямбда без параметров, ссылка на метод |
| 2, 3 | Predicate<T> | boolean test(T t) | Композиция: and(), or(), negate() |
| 4 | Consumer<T> | void accept(T t) | Цепочка действий через andThen() |
| 5 | Function<T, R> | R apply(T t) | Преобразование значения одного типа в другой |
| 6 | Supplier<T> | T get() | Поставщик значения без входных параметров |
Общая теория — в уроке лямбда-выражения в Java.
Задача 1. Лямбда-выражение для интерфейса Printable
Объявите свой интерфейс Printable с единственным методом void print() и создайте его реализацию в виде лямбда-выражения, которое выводит произвольное сообщение в консоль.
@FunctionalInterface
interface Printable {
void print();
}
// Printable printable = ...;
// printable.print();
Обратите внимание: у метода нет параметров, поэтому список параметров лямбды — пустые скобки. Аннотация @FunctionalInterface не обязательна, но заставит компилятор проверить, что абстрактный метод действительно ровно один.
Задача 2. Проверка строки на null и пустоту (Predicate.and)
Задание состоит из трёх шагов и выполняется на функциональном интерфейсе Predicate:
- Напишите лямбда-выражение
Predicate<String>, которое возвращаетtrue, если строка не равнаnull. - Напишите второе лямбда-выражение
Predicate<String>, которое возвращаетtrue, если строка не пустая. - Скомбинируйте оба предиката методом по умолчанию
and()в одну проверку «строка неnullи не пустая» и протестируйте её на значенияхnull,""и"Java".
Порядок важен
Метод and() реализован как t -> test(t) && other.test(t), то есть работает с коротким замыканием слева направо. Предикат, который защищает от null, должен стоять первым, иначе третий пункт задачи будет падать на входном значении null.
Задача 3. Первая и последняя буква строки (Predicate.or)
Напишите программу, которая проверяет, что строка начинается с заглавной буквы 'J' или 'N' и при этом заканчивается заглавной буквой 'A'. Проверку соберите из отдельных Predicate<String> с помощью методов or() и and().
Проверьте результат на строках "Java", "Nika", "Scala" и "java". Регистр учитывается: "java" должна вернуть false.
Задача 4. Consumer и метод andThen
Есть класс HeavyBox с полем «вес». Напишите лямбда-выражение типа Consumer<HeavyBox>, которое принимает объект HeavyBox и выводит в консоль сообщение «Отгрузили ящик с весом n», где n — вес этого ящика.
Затем создайте второй Consumer<HeavyBox> (например, выводящий адрес доставки) и объедините оба действия в цепочку методом по умолчанию andThen(). Убедитесь, что оба сообщения выводятся для одного и того же объекта и в правильном порядке.
Задача 5. Знак числа через Function
Напишите лямбда-выражение типа Function<Integer, String>, которое принимает число и возвращает строку «Положительное число», «Отрицательное число» или «Ноль».
Function<Integer, String> sign = n -> /* ваш код */;
System.out.println(sign.apply(7)); // Положительное число
System.out.println(sign.apply(-3)); // Отрицательное число
System.out.println(sign.apply(0)); // Ноль
Задача 6. Случайное число через Supplier
Напишите лямбда-выражение типа Supplier<Integer>, которое возвращает случайное целое число от 0 до 10 включительно. Вызовите get() несколько раз в цикле и убедитесь, что значения меняются, а границы диапазона не нарушаются.
Задача 7. Ссылка на статический метод
Вернитесь к задаче 1 и перепишите реализацию Printable через ссылку на статический метод: вынесите тело лямбды в отдельный static-метод и передайте его как ClassName::methodName.
Когда ссылка лучше лямбды
Ссылка на метод подходит только тогда, когда лямбда ничего не делает, кроме вызова одного существующего метода с теми же аргументами. Как только внутри появляется хоть какая-то дополнительная логика — условие, форматирование, второй вызов — оставляйте обычное лямбда-выражение, иначе придётся плодить искусственные методы-обёртки.
Как проверить свои решения
Перед тем как сверяться с видео, пройдитесь по короткому чек-листу — он ловит большую часть типичных промахов новичков:
- Скомпилировался ли код без предупреждений и без явного приведения типов? Если требуется каст — скорее всего, выбран не тот функциональный интерфейс.
- Проверены ли граничные значения:
null, пустая строка, ноль, отрицательное число? - Не изменяется ли внутри лямбды локальная переменная внешнего метода? Такой код не скомпилируется: захватываемые переменные должны быть effectively final.
- Нет ли фигурных скобок и
returnтам, где хватило бы однострочного выражения? - Соответствует ли оформление рекомендациям по оформлению кода?
Часто задаваемые вопросы
Что такое лямбда-выражение в Java простыми словами?
Это короткая запись вида параметры -> тело, которая заменяет анонимный класс, реализующий функциональный интерфейс. Компилятор сам понимает, какой метод вы реализуете, потому что у такого интерфейса есть ровно один абстрактный метод. Лямбды доступны начиная с Java 8.
Чем лямбда-выражение отличается от анонимного класса?
Анонимный класс создаёт отдельный класс и собственную область видимости, поэтому this внутри него указывает на сам анонимный объект. В лямбде this ссылается на объемлющий класс, отдельный class-файл не создаётся, а вызов транслируется через invokedynamic. Кроме того, анонимный класс может реализовать интерфейс с любым числом методов, а лямбда — только функциональный.
Почему в лямбда-выражении нельзя менять локальную переменную?
Лямбда захватывает копию значения локальной переменной, а не саму переменную, поэтому Java требует, чтобы переменная была effectively final: ей присваивают значение один раз и больше не меняют. Если счётчик всё же нужен, используйте поле класса, массив из одного элемента или AtomicInteger.
Какой функциональный интерфейс выбрать: Predicate, Function, Consumer или Supplier?
Смотрите на вход и выход. Есть аргумент и нужен boolean — Predicate. Есть аргумент и нужно значение другого типа — Function. Есть аргумент, а результата нет, только действие — Consumer. Аргумента нет, а значение вернуть надо — Supplier. Для примитивов берите специализации вроде IntPredicate или IntSupplier, чтобы избежать лишней упаковки.
Комментарии