2.x. functional
functional
Материалы
Функциональные интерфейсы и лямбда-выражения — основа функционального программирования в Java, введённая в Java 8 (JSR-335). Они позволяют передавать поведение как данные, делая код лаконичнее и выразительнее.
Функциональные интерфейсы
Функциональный интерфейс — это интерфейс с ровно одним абстрактным методом (SAM — Single Abstract Method). Этот метод называется функциональным методом.
// Функциональный интерфейс
interface Converter {
String convert(int value);
}
// Использование с лямбдой
Converter hexConverter = value -> Integer.toHexString(value);
String result = hexConverter.convert(255); // "ff"
Правила определения
Интерфейс считается функциональным, если:
- Имеет ровно один абстрактный метод
- Может иметь любое количество
defaultиstaticметодов - Может иметь абстрактные методы из
Object(equals,hashCode,toString)
@FunctionalInterface
interface Processor<T> {
// Единственный абстрактный метод — функциональный
T process(T input);
// default-методы не считаются
default T processOrDefault(T input, T defaultValue) {
return input != null ? process(input) : defaultValue;
}
// static-методы не считаются
static <T> Processor<T> identity() {
return t -> t;
}
// Методы из Object не считаются
boolean equals(Object obj);
String toString();
}
Аннотация @FunctionalInterface
Аннотация @FunctionalInterface не обязательна, но рекомендуется — она документирует намерение и заставляет компилятор проверять, что интерфейс действительно функциональный:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
// Ошибка компиляции: два абстрактных метода
@FunctionalInterface
interface Invalid {
void method1();
void method2(); // Компилятор укажет на ошибку
}
Стандартные функциональные интерфейсы
Пакет java.util.function содержит 43 готовых функциональных интерфейса. Основные четыре:
| Интерфейс | Метод | Описание | Пример |
|---|---|---|---|
Function<T,R> | R apply(T t) | Преобразование T → R | String::length |
Consumer<T> | void accept(T t) | Потребление значения | System.out::println |
Supplier<T> | T get() | Поставка значения | ArrayList::new |
Predicate<T> | boolean test(T t) | Проверка условия | String::isEmpty |
Function — преобразование
import java.util.function.Function;
Function<String, Integer> length = s -> s.length();
Function<String, Integer> length2 = String::length; // То же самое
int len = length.apply("Hello"); // 5
// Композиция функций
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> trimAndUpper = trim.andThen(upper);
String result = trimAndUpper.apply(" hello "); // "HELLO"
Function<String, String> upperThenTrim = trim.compose(upper);
// Сначала upper, потом trim
Consumer — потребление
import java.util.function.Consumer;
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // То же самое
printer.accept("Hello"); // Выведет: Hello
// Цепочка потребителей
Consumer<String> log = s -> System.out.println("[LOG] " + s);
Consumer<String> save = s -> database.save(s);
Consumer<String> logAndSave = log.andThen(save);
logAndSave.accept("message");
Supplier — поставка
import java.util.function.Supplier;
Supplier<Double> random = () -> Math.random();
Supplier<Double> random2 = Math::random; // То же самое
double value = random.get();
// Ленивая инициализация
Supplier<Connection> connectionSupplier = () -> {
System.out.println("Создаём соединение...");
return DriverManager.getConnection(url);
};
// Соединение создастся только при вызове get()
Predicate — проверка
import java.util.function.Predicate;
Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isEmpty2 = String::isEmpty; // То же самое
boolean result = isEmpty.test(""); // true
// Комбинирование предикатов
Predicate<String> isNotEmpty = isEmpty.negate();
Predicate<String> isShort = s -> s.length() < 10;
Predicate<String> isLong = s -> s.length() > 100;
Predicate<String> isNotEmptyAndShort = isNotEmpty.and(isShort);
Predicate<String> isEmptyOrLong = isEmpty.or(isLong);
// Фильтрация
List<String> filtered = strings.stream()
.filter(isNotEmptyAndShort)
.toList();
Бинарные версии (Bi-)
Для операций с двумя аргументами:
import java.util.function.*;
BiFunction<String, String, Integer> compare =
(a, b) -> a.compareTo(b);
BiConsumer<String, Integer> printWithIndex =
(s, i) -> System.out.println(i + ": " + s);
BiPredicate<String, String> startsWith =
(s, prefix) -> s.startsWith(prefix);
Операторы (Operator)
Специализированные Function, где входной и выходной типы совпадают:
import java.util.function.*;
// UnaryOperator<T> extends Function<T, T>
UnaryOperator<String> toUpper = String::toUpperCase;
String result = toUpper.apply("hello"); // "HELLO"
// BinaryOperator<T> extends BiFunction<T, T, T>
BinaryOperator<Integer> sum = (a, b) -> a + b;
BinaryOperator<Integer> max = Integer::max;
int total = sum.apply(10, 20); // 30
Примитивные специализации
Для избежания boxing/unboxing существуют специализированные версии:
// Вместо Function<Integer, Integer> — избегаем boxing
IntUnaryOperator square = n -> n * n;
int result = square.applyAsInt(5); // 25
// Вместо Predicate<Integer>
IntPredicate isPositive = n -> n > 0;
// Вместо Consumer<Double>
DoubleConsumer printer = System.out::println;
// Вместо Supplier<Long>
LongSupplier currentTime = System::currentTimeMillis;
// Конвертация между примитивами
IntToDoubleFunction intToDouble = n -> n * 1.5;
ToIntFunction<String> stringLength = String::length;
Соглашения об именовании:
IntXxx,LongXxx,DoubleXxx— аргумент примитивныйToIntXxx,ToLongXxx,ToDoubleXxx— результат примитивныйObjIntConsumer— смешанные типы (объект + примитив)
Лямбда-выражения
Лямбда-выражение — это компактная запись анонимной функции, которая может быть присвоена функциональному интерфейсу.
Синтаксис
// Полная форма
(Type1 param1, Type2 param2) -> { statements; return result; }
// Краткие формы
(param1, param2) -> { statements; return result; } // Типы выводятся
(param1, param2) -> expression // Одно выражение
param -> expression // Один параметр, без скобок
() -> expression // Без параметров
Примеры:
// Полная форма
Comparator<String> comp1 = (String a, String b) -> {
return a.compareToIgnoreCase(b);
};
// Вывод типов
Comparator<String> comp2 = (a, b) -> {
return a.compareToIgnoreCase(b);
};
// Одно выражение — return и {} не нужны
Comparator<String> comp3 = (a, b) -> a.compareToIgnoreCase(b);
// Один параметр — скобки не нужны
Function<String, Integer> length = s -> s.length();
// Без параметров
Runnable task = () -> System.out.println("Running");
// Блок с несколькими операторами
Consumer<String> logger = message -> {
String timestamp = LocalDateTime.now().toString();
System.out.println(timestamp + ": " + message);
};
Явные типы параметров (var в Java 11+)
// Явные типы нужны для аннотаций
BiFunction<String, String, String> concat =
(@NonNull String a, @NonNull String b) -> a + b;
// С Java 11 можно использовать var
BiFunction<String, String, String> concat2 =
(@NonNull var a, @NonNull var b) -> a + b;
Правило: либо все параметры с явными типами, либо все без. Смешивать нельзя:
(String a, b)— ошибка.
Целевой тип (Target Type)
Лямбда-выражение не имеет собственного типа — его тип определяется целевым типом из контекста:
// Контекст присваивания
Runnable r = () -> System.out.println("run");
Callable<String> c = () -> "result";
// Контекст аргумента метода
executor.submit(() -> System.out.println("task"));
// Контекст приведения типа
Object obj = (Runnable) () -> System.out.println("run");
// Контекст возврата
Supplier<Runnable> factory() {
return () -> System.out.println("created");
}
Одна и та же лямбда может соответствовать разным интерфейсам:
// Одна лямбда, разные целевые типы
Runnable r = () -> System.out.println("hello");
Supplier<Void> s = () -> { System.out.println("hello"); return null; };
// Вызов метода
interface Printer { void print(); }
interface Logger { void print(); }
void process(Printer p) { p.print(); }
void process(Logger l) { l.print(); }
// Неоднозначность! Нужно явное приведение:
process((Printer) () -> System.out.println("text"));
Ссылки на методы (Method References)
Ссылки на методы — это сокращённая запись лямбд, которые просто вызывают существующий метод.
Четыре вида ссылок
| Вид | Синтаксис | Эквивалентная лямбда |
|---|---|---|
| Статический метод | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
| Метод экземпляра (конкретного объекта) | instance::method | (args) -> instance.method(args) |
| Метод экземпляра (произвольного объекта) | ClassName::method | (obj, args) -> obj.method(args) |
| Конструктор | ClassName::new | (args) -> new ClassName(args) |
Статический метод
// Лямбда
Function<String, Integer> parse1 = s -> Integer.parseInt(s);
// Ссылка на метод
Function<String, Integer> parse2 = Integer::parseInt;
// Использование
List<Integer> numbers = strings.stream()
.map(Integer::parseInt)
.toList();
Метод конкретного экземпляра
String prefix = "[INFO] ";
// Лямбда
Function<String, String> addPrefix1 = s -> prefix.concat(s);
// Ссылка на метод
Function<String, String> addPrefix2 = prefix::concat;
// Пример с System.out
Consumer<String> printer = System.out::println;
Метод произвольного экземпляра
// Лямбда: первый аргумент становится получателем метода
Comparator<String> comp1 = (a, b) -> a.compareToIgnoreCase(b);
// Ссылка на метод
Comparator<String> comp2 = String::compareToIgnoreCase;
// Унарный случай
Function<String, String> upper1 = s -> s.toUpperCase();
Function<String, String> upper2 = String::toUpperCase;
// Использование
List<String> sorted = names.stream()
.sorted(String::compareToIgnoreCase)
.toList();
Ссылка на конструктор
// Конструктор без аргументов
Supplier<ArrayList<String>> listFactory1 = () -> new ArrayList<>();
Supplier<ArrayList<String>> listFactory2 = ArrayList::new;
// Конструктор с аргументом
Function<String, StringBuilder> sbFactory1 = s -> new StringBuilder(s);
Function<String, StringBuilder> sbFactory2 = StringBuilder::new;
// Выбор конструктора определяется целевым типом
Supplier<Person> noArg = Person::new; // Person()
Function<String, Person> withName = Person::new; // Person(String name)
// Использование
List<Person> people = names.stream()
.map(Person::new)
.toList();
Ссылка на конструктор массива
// Создание массива заданной длины
IntFunction<String[]> arrayFactory1 = n -> new String[n];
IntFunction<String[]> arrayFactory2 = String[]::new;
// Полезно для toArray()
String[] array = stream.toArray(String[]::new);
Захват переменных (Capturing)
Лямбды могут использовать переменные из окружающего контекста, но только если они effectively final.
Effectively final
Переменная effectively final, если она инициализируется один раз и никогда не изменяется:
String prefix = "Hello, "; // effectively final
Consumer<String> greeter = name -> {
System.out.println(prefix + name); // OK: prefix захвачена
};
greeter.accept("World"); // "Hello, World"
String message = "Initial";
Consumer<String> printer = s -> {
System.out.println(message); // Ошибка компиляции!
};
message = "Changed"; // message больше не effectively final
Правила захвата
class Example {
private String field = "field";
void method() {
String local = "local";
Runnable r = () -> {
// this — как в окружающем контексте
System.out.println(this.field); // OK
// Захват локальной переменной
System.out.println(local); // OK, если local effectively final
// Изменение поля объекта
this.field = "new"; // OK, меняем поле, не ссылку
};
}
}
Обход ограничения
Если нужно “изменять” захваченное значение:
// Используем контейнер
int[] counter = {0}; // массив — это ссылка, она не меняется
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.forEach(n -> counter[0] += n);
System.out.println(counter[0]); // 15
// Или AtomicInteger
AtomicInteger atomicCounter = new AtomicInteger(0);
numbers.forEach(n -> atomicCounter.addAndGet(n));
Предупреждение: Изменение состояния в лямбдах может привести к проблемам с потокобезопасностью в параллельных stream’ах.
this в лямбдах
В отличие от анонимных классов, this в лямбде ссылается на окружающий объект, а не на саму лямбду:
class Button {
private String name = "Button";
void setupWithLambda() {
// this — это Button
setOnClick(() -> System.out.println(this.name));
}
void setupWithAnonymous() {
setOnClick(new ClickHandler() {
@Override
public void handle() {
// this — это анонимный ClickHandler!
// Для доступа к Button нужно Button.this.name
System.out.println(Button.this.name);
}
});
}
}
Лямбды vs анонимные классы
| Аспект | Лямбда | Анонимный класс |
|---|---|---|
| Синтаксис | Компактный | Многословный |
this | Окружающий объект | Сам анонимный класс |
| Состояние | Нет полей | Может иметь поля |
| Несколько методов | Нет | Да |
| Наследование от класса | Нет | Да |
| Сериализация | Особые правила | Стандартная |
// Лямбда — кратко
Comparator<String> c1 = (a, b) -> a.length() - b.length();
// Анонимный класс — многословно
Comparator<String> c2 = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
Создание собственных функциональных интерфейсов
Создавайте собственные интерфейсы, когда стандартные не подходят:
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(Function<R, V> after) {
return (a, b, c) -> after.apply(apply(a, b, c));
}
}
// Использование
TriFunction<Integer, Integer, Integer, Integer> sum3 =
(a, b, c) -> a + b + c;
int result = sum3.apply(1, 2, 3); // 6
@FunctionalInterface
interface ThrowingSupplier<T, E extends Exception> {
T get() throws E;
static <T> Supplier<T> unchecked(ThrowingSupplier<T, ?> supplier) {
return () -> {
try {
return supplier.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
}
// Использование с checked исключениями
Supplier<Connection> conn = ThrowingSupplier.unchecked(
() -> DriverManager.getConnection(url)
);
Практические примеры
Стратегия валидации
@FunctionalInterface
interface Validator<T> {
ValidationResult validate(T value);
default Validator<T> and(Validator<T> other) {
return value -> {
ValidationResult result = this.validate(value);
return result.isValid() ? other.validate(value) : result;
};
}
}
// Валидаторы как функции
Validator<String> notEmpty = s ->
s != null && !s.isEmpty()
? ValidationResult.ok()
: ValidationResult.error("Не может быть пустым");
Validator<String> maxLength = s ->
s.length() <= 100
? ValidationResult.ok()
: ValidationResult.error("Слишком длинное");
Validator<String> emailFormat = s ->
s.contains("@")
? ValidationResult.ok()
: ValidationResult.error("Неверный формат email");
// Композиция
Validator<String> emailValidator = notEmpty
.and(maxLength)
.and(emailFormat);
ValidationResult result = emailValidator.validate("user@example.com");
Ленивые вычисления
class Lazy<T> {
private final Supplier<T> supplier;
private T value;
private boolean computed = false;
private Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}
public static <T> Lazy<T> of(Supplier<T> supplier) {
return new Lazy<>(supplier);
}
public T get() {
if (!computed) {
value = supplier.get();
computed = true;
}
return value;
}
public <R> Lazy<R> map(Function<T, R> mapper) {
return Lazy.of(() -> mapper.apply(get()));
}
}
// Использование
Lazy<ExpensiveObject> lazy = Lazy.of(() -> {
System.out.println("Создаём дорогой объект...");
return new ExpensiveObject();
});
// Объект ещё не создан
System.out.println("Перед get()");
ExpensiveObject obj = lazy.get(); // Теперь создаётся
ExpensiveObject obj2 = lazy.get(); // Возвращает кэшированный
Построитель с fluent API
class QueryBuilder {
private final List<Predicate<Record>> filters = new ArrayList<>();
private Comparator<Record> sorter = null;
private int limit = Integer.MAX_VALUE;
public QueryBuilder filter(Predicate<Record> predicate) {
filters.add(predicate);
return this;
}
public QueryBuilder sortBy(Function<Record, Comparable> keyExtractor) {
this.sorter = Comparator.comparing(keyExtractor);
return this;
}
public QueryBuilder limit(int n) {
this.limit = n;
return this;
}
public List<Record> execute(List<Record> data) {
Stream<Record> stream = data.stream();
for (Predicate<Record> filter : filters) {
stream = stream.filter(filter);
}
if (sorter != null) {
stream = stream.sorted(sorter);
}
return stream.limit(limit).toList();
}
}
// Использование
List<Record> results = new QueryBuilder()
.filter(r -> r.getStatus() == Status.ACTIVE)
.filter(r -> r.getAmount() > 1000)
.sortBy(Record::getDate)
.limit(10)
.execute(records);
Резюме
Функциональные интерфейсы и лямбды — мощные инструменты Java:
- Функциональный интерфейс имеет один абстрактный метод и служит целевым типом для лямбд
- @FunctionalInterface документирует намерение и включает проверку компилятора
- java.util.function содержит готовые интерфейсы:
Function,Consumer,Supplier,Predicateи их варианты - Лямбда-выражения — компактная запись анонимных функций
- Ссылки на методы (
::) — ещё более краткая форма для существующих методов - Лямбды захватывают только effectively final переменные
- this в лямбде ссылается на окружающий объект, а не на саму лямбду
- Лямбды компактнее анонимных классов, но ограничены одним методом