Тема 11. Functional Interfaces and Lambdas
Функциональные интерфейсы и лямбды в Java позволяют передавать поведение как значение. Это не замена ООП, а способ писать более гибкий прикладной код: стратегию, фильтр, коллбек, обработчик события. Для новичка главная польза — меньше шаблонного кода и более явные контракты поведения.
1. Базовая идея: SAM-интерфейс
Функциональный интерфейс имеет один абстрактный метод (SAM — Single Abstract Method).
@FunctionalInterface
interface MathOp {
int apply(int a, int b);
}
MathOp add = (a, b) -> a + b;
System.out.println(add.apply(2, 3)); // 5
Аннотация @FunctionalInterface не обязательна, но полезна: компилятор проверит, что интерфейс действительно SAM.
2. Где это используется в реальной Java
- Collections API:
sort,removeIf,forEach. - Stream API:
map,filter,reduce. CompletableFutureи асинхронные пайплайны.- Паттерны Strategy/Callback.
Runnable,Callable,ExecutorService.
3. Стандартные functional interfaces
| Интерфейс | Контракт |
|---|---|
Predicate<T> | T -> boolean |
Function<T, R> | T -> R |
Consumer<T> | T -> void |
Supplier<T> | () -> T |
UnaryOperator<T> | T -> T |
BinaryOperator<T> | (T, T) -> T |
BiFunction<T, U, R> | (T, U) -> R |
Пример:
Predicate<String> longName = s -> s.length() >= 4;
Function<String, String> upper = String::toUpperCase;
Consumer<String> printer = System.out::println;
List.of("ann", "alex").stream()
.filter(longName)
.map(upper)
.forEach(printer);
4. Синтаксис лямбд
() -> 42
x -> x * 2
(a, b) -> a + b
(String s) -> s.trim()
(a, b) -> {
int r = a + b;
return r;
}
Правило читаемости: если лямбда становится длинной, лучше вынести её в именованный метод.
5. Method reference как короткая форма
Когда лямбда просто вызывает существующий метод, удобнее method reference:
List<String> names = new ArrayList<>(List.of("Bob", "Ann", "alex"));
names.sort(String::compareToIgnoreCase);
Формы:
ClassName::staticMethodobj::instanceMethodClassName::instanceMethodClassName::new
6. Захват переменных и effectively final
Лямбды могут читать локальные переменные, если они final или effectively final:
int min = 3;
Predicate<String> p = s -> s.length() >= min;
Нельзя менять min после захвата:
int min = 3;
min++; // после этого переменная не effectively final
Это ограничение защищает от трудноуловимых ошибок времени жизни и конкуренции.
7. Композиция функций и предикатов
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);
System.out.println(pipeline.apply(" java ")); // JAVA
Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> shortWord = s -> s.length() <= 5;
Predicate<String> rule = notBlank.and(shortWord);
Композиция делает правила модульными: их проще тестировать и переиспользовать.
8. Прикладные паттерны
Strategy
interface DiscountStrategy {
double apply(double amount);
}
double checkout(double amount, DiscountStrategy strategy) {
return strategy.apply(amount);
}
double result = checkout(1000, a -> a * 0.9);
Callback wrapper
void withLogging(String opName, Runnable action) {
long start = System.nanoTime();
try {
action.run();
} finally {
System.out.println(opName + " took " + (System.nanoTime() - start));
}
}
9. Частые ошибки
- Лямбда на пол-экрана с тяжелой бизнес-логикой.
- Побочные эффекты внутри stream-операций.
- Непонятные имена аргументов в сложных выражениях.
- Создание своих интерфейсов там, где хватает
java.util.function.
10. Когда лучше обычный метод
Выносите код в метод, если:
- логика повторяется;
- лямбда стала длинной;
- нужно отдельное unit-тестирование;
- имя метода лучше объясняет намерение, чем inline-лямбда.
Что важно запомнить
Функциональные интерфейсы — это контракт поведения, лямбды — компактная форма передачи этого поведения. Сила подхода раскрывается, когда лямбды короткие, чистые и композиционные, а сложная логика вынесена в именованные методы.