JavaRush /Курсы /JAVA 25 SELF /Функциональный стиль со Stream API

Функциональный стиль со Stream API

JAVA 25 SELF
49 уровень , 3 лекция
Открыта

1. Императивный vs функциональный стиль

Давайте начнем с простого вопроса: а зачем вообще нужен этот самый «функциональный стиль»? Чем он лучше обычного, привычного подхода через циклы? И что вообще значит «функциональный стиль» в Java?

Императивный стиль

Императивный стиль — это когда вы говорите компьютеру как что-то сделать, шаг за шагом. Например, если нужно из списка строк получить список длин этих строк, оставить только нечётные длины и отсортировать их по убыванию, вы пишете примерно так:

List<String> words = Arrays.asList("кот", "слон", "носорог", "тигр", "мышь");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

Здесь мы явно создаём промежуточный список, вручную добавляем элементы, сортируем — всё по шагам.

Функциональный стиль

Функциональный стиль — это когда вы описываете что вы хотите получить, а не как это делается. В Java это реализовано через Stream API:

List<String> words = Arrays.asList("кот", "слон", "носорог", "тигр", "мышь");

List<Integer> result = words.stream()
    .map(String::length)
    .filter(len -> len % 2 != 0)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result); // [7, 5, 3]

Здесь мы как будто строим «конвейер» обработки данных: сначала превращаем слова в их длины (map), потом фильтруем нечётные (filter), потом сортируем (sorted). Всё это — в одной цепочке, без явных промежуточных коллекций и циклов.

Сравнение: итеративный vs функциональный стиль

В привычном императивном подходе мы пишем цикл, внутри которого по шагам объясняем компьютеру, что делать: пройтись по каждому элементу, проверить условие, добавить в новый список или вывести на экран. Код работает, но занимает больше строк, и чем сложнее задача, тем труднее его читать и сопровождать.

Функциональный стиль позволяет описывать не сам процесс, а то, что мы хотим получить. Вместо длинного цикла мы строим цепочку операций: отфильтровать, преобразовать, собрать результат. Это короче, нагляднее и уменьшает риск ошибок, потому что меньше «ручной работы» с изменяемыми коллекциями.

Есть и обратная сторона. Для новичка такая цепочка может выглядеть запутанной: несколько лямбд подряд читаются сложнее, чем простой цикл. Поэтому функциональный стиль выигрывает в краткости и выразительности, но требует привычки и насмотренности.

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

Stream API — это не просто «новый вид цикла», а целый набор инструментов для обработки коллекций в функциональном стиле. Давайте разберёмся с основными операциями.

Как получить Stream?

List<String> list = List.of("a", "bb", "ccc");
Stream<String> stream = list.stream();

Промежуточные операции

  • map — преобразует элементы потока
  • filter — фильтрует элементы по условию
  • flatMap — превращает каждый элемент в поток и «разворачивает» их
  • sorted — сортировка
  • distinct — убирает дубликаты
  • limit / skip — ограничить/пропустить элементы

Терминальные операции

  • forEach — выполнить действие для каждого элемента
  • collect — собрать результат в коллекцию
  • reduce — свести поток к одному значению (например, сумме)
  • count — посчитать количество элементов
  • anyMatch, allMatch, noneMatch — проверки условий

Пример: цепочка обработки

List<String> names = List.of("Анна", "Борис", "Вика", "Глеб", "Даша");

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

System.out.println(filtered); // [БОРИС, ВИКА, ДАША]

Визуальная схема «конвейера»:

[Анна, Борис, Вика, Глеб, Даша]
   | filter (length>3)
[Борис, Вика, Даша]
   | map (toUpperCase)
[БОРИС, ВИКА, ДАША]
   | sorted
[БОРИС, ВИКА, ДАША]
   | toList

Каждая операция не меняет исходную коллекцию — создаётся новый поток.

3. Потоковое выполнение и ленивость

Промежуточные и терминальные операции

У Stream’ов есть два типа операций. Первые — промежуточные, такие как map, filter или sorted. Они возвращают новый поток и как будто обещают что-то сделать, но на самом деле ничего ещё не выполняют. Вторые — терминальные, например forEach, collect или reduce. Вот они уже действительно запускают всю обработку. Ключевой момент в том, что пока вы не вызвали терминальную операцию, поток остаётся «ленивым» — вычисления не начинаются.

Пример:

Stream<String> stream = List.of("a", "bb", "ccc").stream()
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

System.out.println("До forEach");
stream.forEach(System.out::println);

Вывод:

До forEach
map: a
A
map: bb
BB
map: ccc
CCC

Видно, что map не выполняется, пока не начался forEach.

Почему это круто?

Такой подход позволяет без лишних затрат строить сколько угодно длинные цепочки преобразований. Поток начнёт работать только тогда, когда это действительно понадобится. Благодаря этому можно без проблем обрабатывать огромные объёмы данных или даже бесконечные последовательности. А ещё ленивость помогает экономить память и выполнять вычисления эффективнее.

4. Практика: задача «Строки → длины → нечётные → по убыванию»

Давайте пошагово решим задачу: «Из списка строк получить список длин строк, оставить только нечётные длины, отсортировать по убыванию.»

Императивное решение

List<String> words = Arrays.asList("кот", "слон", "носорог", "тигр", "мышь");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [7, 5, 3]

Функциональное решение с Stream API

List<String> words = Arrays.asList("кот", "слон", "носорог", "тигр", "мышь");

List<Integer> result = words.stream()
    .map(String::length) // преобразуем строки в их длины
    .filter(len -> len % 2 != 0) // оставляем только нечётные длины
    .sorted(Comparator.reverseOrder()) // сортируем по убыванию
    .toList(); // собираем в List (Java 16+)

System.out.println(result); // [7, 5, 3]

Пояснения:

  • map(String::length) — для каждой строки получаем её длину.
  • filter(len -> len % 2 != 0) — оставляем только нечётные длины.
  • sorted(Comparator.reverseOrder()) — сортируем по убыванию.
  • toList() — собираем поток в новый список.
Аналогия

Это как если бы вы строили «ленту» на заводе: на каждом этапе детали обрабатываются по-новому, и только в самом конце всё складывается в коробку.

5. Ещё примеры: map, filter, forEach, collect

Пример 1: Фильтрация и печать

List<String> names = List.of("Анна", "Борис", "Вика", "Глеб", "Даша");

names.stream()
    .filter(name -> name.contains("а"))
    .forEach(System.out::println);
// Выведет: Анна, Даша

Пример 2: Преобразование и сборка в Set

Set<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toSet());

System.out.println(upperNames); // [АННА, БОРИС, ВИКА, ГЛЕБ, ДАША]

Пример 3: Получение суммы длин всех строк

int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

System.out.println("Суммарная длина: " + totalLength);

Пример 4: Использование Predicate и Function

Predicate<String> longName = name -> name.length() > 4;
Function<String, String> greet = name -> "Привет, " + name + "!";

names.stream()
    .filter(longName)
    .map(greet)
    .forEach(System.out::println);
// Привет, Борис!
// Привет, Даша!

6. Особенности функционального стиля с Stream API

Отсутствие изменяемого состояния

Стиль Stream API поощряет «чистые» функции — без побочных эффектов. Это значит, что лучше не менять внешние переменные внутри лямбд.

Плохо:

List<String> result = new ArrayList<>();
names.stream()
    .filter(name -> name.startsWith("А"))
    .forEach(result::add); // side-effect!

Лучше:

List<String> result = names.stream()
    .filter(name -> name.startsWith("А"))
    .toList();

Композиция операций

Можно строить очень длинные цепочки, комбинируя map, filter, sorted и другие методы. Главное — не увлекаться: если цепочка стала длиннее экрана, возможно, стоит разбить её на части.

Ленивость вычислений

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

Неизменяемость исходных коллекций

Stream не меняет исходную коллекцию! Все преобразования возвращают новый поток/коллекцию.

7. Когда использовать Stream API

Stream API отлично подходит, когда:

  • Нужно быстро обработать коллекцию (фильтрация, преобразование, сортировка).
  • Требуется лаконичный, читаемый код.
  • Не хочется вручную создавать промежуточные коллекции.
  • Нужно легко добавить параллелизм (через parallelStream()).

Императивный стиль иногда предпочтительнее, если:

  • Необходима сложная логика с несколькими вложенными циклами и условиями.
  • Важно сохранить максимальную производительность в критичных местах (Stream API иногда чуть медленнее).
  • Нужно работать с изменяемым состоянием (например, обновлять элементы «на месте»).

8. Типичные ошибки при работе с Stream API

Ошибка №1: Использование forEach для сбора коллекции. Многие новички используют forEach для добавления элементов в новую коллекцию. Это не функциональный стиль! Вместо этого используйте collect или toList().

Ошибка №2: Преждевременная оптимизация. Не пытайтесь сразу использовать parallelStream() — параллелизм нужен только для действительно больших коллекций и CPU-интенсивных задач.

Ошибка №3: Смешивание Stream API и мутабельных коллекций. Stream API подразумевает работу с неизменяемыми данными. Не стоит внутри лямбд менять элементы коллекции.

Ошибка №4: «Потеря» результата. Забыли вызвать терминальную операцию — ничего не произойдёт.

Ошибка №5: Слишком сложные лямбды. Если лямбда-выражение стало длиннее одной-двух строк — вынесите его в отдельный метод с понятным именем.

1
Задача
JAVA 25 SELF, 49 уровень, 3 лекция
Недоступна
Оценка "Магического Инвентаря" в Текстовом Квесте 🧙‍♂️
Оценка "Магического Инвентаря" в Текстовом Квесте 🧙‍♂️
1
Задача
JAVA 25 SELF, 49 уровень, 3 лекция
Недоступна
Анализ Населённых Пунктов для Загадочной Экспедиции 🗺️
Анализ Населённых Пунктов для Загадочной Экспедиции 🗺️
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ