Перейти к содержимому
Теория · Лямбды и Stream API · урок 7 из 12

Stream API: filter, map, collect

До сих пор каждая станция работала сама по себе: removeIf удалял, forEach печатал, sort сортировал. Теперь соберём конвейер целиком.

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

JAVA
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("cat", "zebra", "ox", "horse");
        List<String> result = words.stream()
            .filter(w -> w.length() > 2)
            .map(w -> w.toUpperCase())
            .toList();
        System.out.println(result); // [CAT, ZEBRA, HORSE]
        System.out.println(words);  // [cat, zebra, ox, horse]
    }
}

Давайте разберём программу по шагам:

  1. words.stream() выкладывает слова списка на ленту — создаёт стрим.
  2. filter(w -> w.length() > 2) — станция, которая пропускает дальше только слова длиннее двух букв. Слово ox снимается с ленты.
  3. map(w -> w.toUpperCase()) — станция, которая заменяет каждое слово новым, написанным заглавными буквами.
  4. toList() — сборщик в конце: он складывает всё, что доехало, в новый список.
  5. Первая печать даёт [CAT, ZEBRA, HORSE], а вторая показывает, что words не изменился.

Что такое стрим

Стрим — конвейер, по которому едут элементы источника, например списка. Стрим не хранит элементы и не меняет источник: filter не удаляет ox из words, а только не пускает его дальше по ленте.

Карточки станций вам уже знакомы. filter ждёт Predicate и пропускает элемент, если для него Predicate вернул true. map ждёт Function и ставит на ленту вместо элемента результат этой Function.

Здесь картинка с конвейером неточна. На настоящей станции работник перекладывает ту же коробку в новую упаковку. map же выпускает на ленту новый объект, а исходная строка остаётся какой была.

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

Методы стрима бывают двух видов, и на собеседовании часто просят их различить.

Промежуточные

Промежуточная операция — станция на ленте. Она возвращает новый стрим, поэтому за ней можно поставить следующую. filter и map — промежуточные операции.

Терминальные

Терминальная операция — сборщик в конце ленты. Она возвращает уже не стрим, а результат: список, число или ничего. После неё цепочку не продолжить, поэтому терминальная операция стоит последней. toList() — терминальная операция.

Метод toList() появился в Java 16. В более старом коде вместо него пишут collect(Collectors.toList()): метод collect собирает элементы, а Collectors.toList() говорит ему, во что. Такую запись вы часто встретите в рабочих проектах. Список из toList() менять нельзя, как и список из List.of: его add бросит UnsupportedOperationException.

Итог одним числом

Иногда в конце ленты нужен не список, а одно число:

JAVA
long n = words.stream().filter(w -> w.length() > 3).count();
System.out.println(n); // 2
int letters = words.stream().mapToInt(String::length).sum();
System.out.println(letters); // 15

Разберём по строкам:

  1. count() — терминальная операция: она считает, сколько элементов доехало до конца. Доехали zebra и horse, поэтому результат 2. Тип результата — long.
  2. mapToInt(String::length) заменяет каждое слово числом int — его длиной. Получается особый стрим чисел, IntStream.
  3. sum() — терминальная операция IntStream: она складывает 3 + 5 + 2 + 5 и даёт 15.

Почему mapToInt, а не map? У обычного стрима нет метода sum: он не знает, что его элементы — числа. IntStream это знает, поэтому sum у него есть.

Частая ошибка

Результат count() положили в переменную int:

JAVA
int n = words.stream().filter(w -> w.length() > 3).count(); // ✗
TEXT
error: incompatible types: possible lossy conversion from long to int

Программа не компилируется: count() возвращает long, а в int такое число может не поместиться, и сам сужать тип компилятор не станет. Похожее сообщение вы уже видели в теме про операторы, только там был double. Объявите переменную как long n.

Главное

  • Стрим — конвейер над элементами источника: stream() выкладывает их на ленту, а сам источник не меняется.
  • Промежуточные операции (filter, map) возвращают новый стрим, терминальные (toList, count, sum) — результат.
  • filter пропускает элементы, для которых Predicate вернул true, а map заменяет каждый элемент результатом Function.
  • count() возвращает long, а mapToInt(...).sum() складывает числа.