Какие промежуточные операции есть в Stream API Java?

«Какие промежуточные операции есть в Stream API Java?» — вопрос из категории Java Core, который задают на 22% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Промежуточные операции (intermediate operations) в Stream API возвращают новый поток и являются "ленивыми" — они выполняются только при вызове терминальной операции.

Основные промежуточные операции:

Операция Сигнатура Назначение
Фильтрация filter(Predicate<T>) Отбирает элементы, удовлетворяющие условию.
Преобразование map(Function<T, R>) Преобразует каждый элемент в другой объект.
"Разворачивание" flatMap(Function<T, Stream<R>>) Преобразует элемент в поток и объединяет все потоки в один.
Уникальность distinct() Удаляет дубликаты (использует equals() и hashCode()).
Сортировка sorted() или sorted(Comparator<T>) Сортирует элементы (естественным порядком или компаратором).
Просмотр peek(Consumer<T>) Выполняет действие над каждым элементом без изменения потока (для отладки).
Ограничение limit(long maxSize) Ограничивает поток первыми N элементами.
Пропуск skip(long n) Пропускает первые N элементов потока.

Практические примеры:

List<String> names = List.of("John", "Alice", "Bob", "Alice", "Charlie");

// Пример 1: Фильтрация, преобразование, удаление дубликатов
List<String> result = names.stream()
        .filter(name -> name.length() > 3)      // Оставляет "John", "Alice", "Charlie"
        .map(String::toUpperCase)               // Преобразует в "JOHN", "ALICE", "CHARLIE"
        .distinct()                             // Удаляет дубликаты (если бы были)
        .collect(Collectors.toList());          // Терминальная операция
// Результат: ["JOHN", "ALICE", "CHARLIE"]

// Пример 2: Использование flatMap для работы с вложенными структурами
List<List<Integer>> listOfLists = List.of(
        List.of(1, 2),
        List.of(3, 4, 5)
);
List<Integer> flattened = listOfLists.stream()
        .flatMap(List::stream) // Преобразует каждый List в Stream и объединяет
        .collect(Collectors.toList());
// Результат: [1, 2, 3, 4, 5]

// Пример 3: Отладка с помощью peek
long count = names.stream()
        .peek(name -> System.out.println("Before filter: " + name))
        .filter(name -> name.startsWith("A"))
        .peek(name -> System.out.println("After filter: " + name))
        .count();
// Вывод в консоль поможет отследить путь элементов через поток.

Важно: Промежуточные операции можно объединять в цепочки, создавая декларативное описание pipeline для обработки данных.