Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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"

Правила определения

Интерфейс считается функциональным, если:

  1. Имеет ровно один абстрактный метод
  2. Может иметь любое количество default и static методов
  3. Может иметь абстрактные методы из 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 → RString::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 в лямбде ссылается на окружающий объект, а не на саму лямбду
  • Лямбды компактнее анонимных классов, но ограничены одним методом