Функциональное программирование в Java: от лямбд до Stream API

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в Java: от лямбд до Stream API

Функциональное программирование (ФП) — это парадигма, в которой вычисления представляются как вычисление математических функций, избегая изменения состояния и мутации данных. Долгое время Java оставалась строго объектно-ориентированным языком, но с выходом Java 8 (2014 год) она сделала огромный шаг в сторону ФП, добавив лямбда-выражения, функциональные интерфейсы и Stream API. Сегодня эти инструменты незаменимы для написания лаконичного, читаемого и параллельно-безопасного кода.

В этой статье мы разберём ключевые концепции функционального программирования в Java: от основ лямбд до продвинутой обработки коллекций с помощью Stream API. Материал ориентирован на разработчиков, которые уже знакомы с базовым синтаксисом Java, но хотят освоить современные подходы.

1. Лямбда-выражения и функциональные интерфейсы

Лямбда-выражение — это анонимная функция (метод без имени), которую можно передавать как аргумент или сохранять в переменную. В Java лямбда реализует функциональный интерфейс — интерфейс с ровно одним абстрактным методом (SAM — Single Abstract Method).

Синтаксис:

(параметры) -> { тело }

Пример: вместо создания анонимного класса для компаратора:

// Старый способ (Java 7)
Comparator<String> comparator = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

// Новый способ (Java 8+)
Comparator<String> comparatorLambda = (s1, s2) -> s1.length() - s2.length();

Основные функциональные интерфейсы в Java (пакет java.util.function):

  • Predicate<T> — проверяет условие (метод test).
  • Function<T, R> — преобразует объект типа T в R (метод apply).
  • Consumer<T> — выполняет действие над объектом (метод accept).
  • Supplier<T> — поставляет объект без входных параметров (метод get).

Пример с Predicate:

Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(5)); // false

Ссылки на методы (Method References) — это сокращённая запись лямбды, когда тело состоит из одного вызова метода:

// Лямбда
list.forEach(item -> System.out.println(item));
// Ссылка на метод
list.forEach(System.out::println);

2. Stream API: основные операции

Stream API — это мощный инструмент для обработки последовательностей данных (коллекций, массивов, файлов) в декларативном стиле. Stream не хранит данные, а только описывает конвейер операций. Данные не изменяются — каждый шаг создаёт новый стрим.

Создание стрима:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream(); // последовательный
Stream<String> parallelStream = names.parallelStream(); // параллельный

Промежуточные операции (возвращают новый Stream):

  • filter(Predicate) — отбирает элементы по условию.
  • map(Function) — преобразует каждый элемент.
  • sorted() / sorted(Comparator) — сортировка.
  • distinct() — удаляет дубликаты.
  • limit(n) — ограничивает количество элементов.

Терминальные операции (завершают стрим, возвращают результат):

  • collect(Collectors.toList()) — собирает в коллекцию.
  • forEach(Consumer) — выполняет действие для каждого элемента.
  • count() — возвращает количество элементов.
  • anyMatch(Predicate) — проверяет, удовлетворяет ли хотя бы один элемент условию.
  • reduce(BinaryOperator) — свёртка (агрегация).

Пример: отфильтровать имена длиннее 4 символов, преобразовать в верхний регистр и собрать в список:

List<String> result = names.stream()
    .filter(name -> name.length() > 4)
    .map(String::toUpperCase)
    .collect(Collectors.toList());
System.out.println(result); // [ALICE, CHARLIE]

3. Продвинутые техники: reduce, groupingBy и параллельные стримы

Операция reduce — это мощный инструмент для агрегации. Она принимает начальное значение (identity) и функцию-аккумулятор:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println(sum); // 15

Группировка с Collectors.groupingBy:

Map<Integer, List<String>> groupedByLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
System.out.println(groupedByLength);
// {3=[Bob], 5=[Alice], 7=[Charlie]}

Параллельные стримы позволяют легко распараллелить вычисления для многоядерных процессоров. Однако следует помнить, что они эффективны только для больших объёмов данных и при отсутствии побочных эффектов:

long count = names.parallelStream()
    .filter(name -> name.startsWith("A"))
    .count();

Важно: при использовании параллельных стримов избегайте общих изменяемых переменных — это приводит к состояниям гонки (race conditions). Используйте потокобезопасные коллекции или атомарные переменные.

4. Практический пример: обработка списка заказов

Блоги

Рекомендаций по книгам