Функциональное программирование в Java: продвинутый уровень
Функциональное программирование (ФП) — это парадигма, в которой вычисления рассматриваются как вычисление математических функций, избегающих изменения состояния и изменяемых данных. Java, будучи объектно-ориентированным языком, с выходом Java 8 получила мощные инструменты для ФП: лямбда-выражения, Stream API и функциональные интерфейсы. На продвинутом уровне эти возможности позволяют писать более лаконичный, безопасный и выразительный код, особенно при работе с коллекциями, асинхронными операциями и обработкой ошибок.
В этой статье мы рассмотрим продвинутые техники функционального программирования в Java: композицию функций, монады (Optional и CompletableFuture), Stream API с параллельной обработкой и каррирование.
1. Композиция функций и функциональные интерфейсы
В Java функциональные интерфейсы — это интерфейсы с одним абстрактным методом (SAM). Наиболее часто используемые: Function<T, R>, Predicate<T>, Consumer<T> и Supplier<T>. Продвинутое использование включает композицию функций с помощью методов andThen() и compose().
Пример: цепочка преобразований
import java.util.function.Function;
public class FunctionComposition {
public static void main(String[] args) {
// Функция: строка -> её длина
Function<String, Integer> lengthFunction = String::length;
// Функция: число -> его квадрат
Function<Integer, Integer> squareFunction = x -> x * x;
// Композиция: строка -> длина -> квадрат длины
Function<String, Integer> composedFunction = lengthFunction.andThen(squareFunction);
String text = "Hello, Java!";
int result = composedFunction.apply(text); // 12 * 12 = 144
System.out.println("Результат: " + result); // 144
}
}
Метод andThen() применяет первую функцию, затем вторую. compose() делает обратное: сначала применяет переданную функцию, затем первую. Это позволяет строить гибкие и переиспользуемые цепочки преобразований.
Практический пример: фильтрация и преобразование списка
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateAndFunction {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve");
Predicate<String> startsWithA = s -> s.startsWith("A");
Function<String, String> toUpperCase = String::toUpperCase;
List<String> result = names.stream()
.filter(startsWithA)
.map(toUpperCase)
.collect(Collectors.toList());
System.out.println(result); // [ALICE]
}
}
2. Монады в Java: Optional и CompletableFuture
Монада — это паттерн, который инкапсулирует значение и предоставляет операции для его преобразования без явного разворачивания. В Java монады реализованы через Optional (для обработки null) и CompletableFuture (для асинхронных операций).
Optional как монада
Методы map(), flatMap() и filter() позволяют цепочечно работать с возможным отсутствием значения.
import java.util.Optional;
public class OptionalMonad {
public static void main(String[] args) {
// Создаем Optional с непустым значением
Optional<String> optional = Optional.of("Hello");
// Цепочка преобразований
String result = optional
.filter(s -> s.length() > 3) // фильтр
.map(String::toUpperCase) // преобразование
.orElse("Default"); // значение по умолчанию
System.out.println(result); // HELLO
// Пример с пустым Optional
Optional<String> empty = Optional.empty();
String emptyResult = empty
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.orElse("Default");
System.out.println(emptyResult); // Default
}
}
CompletableFuture для асинхронных цепочек
CompletableFuture позволяет объединять асинхронные задачи, обрабатывать ошибки и комбинировать результаты.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class CompletableFutureMonad {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Асинхронное получение данных
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000); // имитация долгой операции
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Data from remote";
});
// Цепочка преобразований
CompletableFuture<Integer> processed = future
.thenApply(String::length) // преобразование
.thenApply(len -> len * 2); // ещё одно
System.out.println(processed.get()); // 32 (16 символов * 2)
}
}
3. Продвинутый Stream API: параллельные потоки и кастомные коллекторы
Stream API — основа ФП в Java. На продвинутом уровне важны параллельные потоки (parallelStream()) и создание собственных коллекторов.
Параллельные потоки
Параллельные потоки используют ForkJoinPool для ускорения обработки больших данных. Однако они требуют аккуратности из-за состояния гонки и порядка выполнения.
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ParallelStreamExample {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
// Параллельная обработка
long sum = numbers.parallelStream()
.filter(n -> n % 2 == 0) // чётные числа
.mapToLong(Integer::longValue)
.sum();
System.out.println("Сумма чётных чисел: " + sum); // 2550
}
}
Кастомный коллектор
С помощью Collector.of() можно создать свой коллектор для специфических задач.
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import