Функциональное программирование в 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). Используйте потокобезопасные коллекции или атомарные переменные.