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

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

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

Начать курс

Введение в функциональное программирование в Java

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

Функциональное программирование в Java — это не замена ООП, а мощное дополнение. Оно позволяет писать декларативный код, где вы описываете что нужно сделать, а не как. В этой статье мы разберём ключевые концепции, которые помогут вам освоить функциональный стиль на Java, и покажем практические примеры.

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

Основой функционального программирования в Java являются лямбда-выражения — это компактный способ реализации анонимных функций. Синтаксически лямбда состоит из списка параметров, стрелки (->) и тела:

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

Лямбды всегда работают в паре с функциональными интерфейсами. Функциональный интерфейс — это интерфейс, который содержит ровно один абстрактный метод (например, Runnable, Comparator, Predicate).

Практический пример: замена анонимного класса

Рассмотрим сравнение старого (ООП) и нового (функционального) подхода:

// Старый способ (анонимный класс)
Runnable taskOld = new Runnable() {
    @Override
    public void run() {
        System.out.println("Привет из анонимного класса!");
    }
};

// Новый способ (лямбда)
Runnable taskNew = () -> System.out.println("Привет из лямбды!");

// Запуск
taskNew.run();

Как видите, лямбда сократила код с 6 строк до 1, сохранив при этом полную функциональность.

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

Java предоставляет богатый набор встроенных интерфейсов. Вот четыре основных:

  • Predicate<T> — принимает объект, возвращает boolean. Используется для фильтрации.
  • Function<T, R> — принимает объект одного типа, возвращает объект другого типа. Для преобразований.
  • Consumer<T> — принимает объект, ничего не возвращает (void). Для побочных эффектов (вывод на печать).
  • Supplier<T> — не принимает ничего, возвращает объект. Для ленивой генерации данных.

Пример с Predicate:

import java.util.function.Predicate;

public class Main {
    public static void main(String[] args) {
        Predicate<Integer> isEven = n -> n % 2 == 0;
        System.out.println(isEven.test(10)); // true
        System.out.println(isEven.test(7));  // false
    }
}

2. Stream API: работа с коллекциями в функциональном стиле

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

Stream API строится на трёх этапах:

  1. Создание потока (из коллекции, массива или генератора).
  2. Промежуточные операции (фильтрация, сортировка, преобразование). Возвращают новый Stream.
  3. Терминальная операция (сбор результата, подсчёт, поиск). Завершает работу потока.

Пример: фильтрация и преобразование списка

Допустим, у нас есть список строк, и мы хотим получить только те, которые длиннее 3 символов, преобразовать их в верхний регистр и отсортировать.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Java", "Python", "C", "JavaScript", "Go");

        List<String> result = words.stream()
                .filter(s -> s.length() > 3)          // Промежуточная: фильтр
                .map(String::toUpperCase)              // Промежуточная: преобразование
                .sorted()                              // Промежуточная: сортировка
                .collect(Collectors.toList());         // Терминальная: сбор в список

        System.out.println(result); // [JAVA, JAVASCRIPT, PYTHON]
    }
}

Обратите внимание на String::toUpperCase — это ссылка на метод, ещё одна полезная синтаксическая конструкция, которая делает код ещё короче.

Параллельные потоки

Stream API упрощает многопоточную обработку данных. Достаточно заменить .stream() на .parallelStream(), и Java сама распределит вычисления по ядрам процессора:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

int sum = numbers.parallelStream()
                 .filter(n -> n % 2 == 0)
                 .mapToInt(Integer::intValue)
                 .sum();

System.out.println("Сумма чётных чисел: " + sum); // 30

3. Optional: борьба с NullPointerException

Ещё одна важная функциональная фича Java 8 — класс Optional<T>. Он создан для того, чтобы явно указывать, что значение может отсутствовать, и избежать знаменитого NullPointerException.

Optional предоставляет функциональные методы для безопасной работы с потенциально null-значениями:

  • isPresent() — проверяет, есть ли значение.
  • ifPresent(Consumer) — выполняет действие, если значение есть.
  • orElse(T) — возвращает значение или значение по умолчанию.
  • map(Function) — преобразует значение, если оно есть.

Пример: поиск

Блоги

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