← Назад к вопросам

В чём разница между терминальными и нетерминальными (промежуточными) операциями в Stream API?

2.0 Middle🔥 201 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Терминальные и нетерминальные операции в Stream API

Stream API в Java делит операции на две категории. Понимание разницы критично для использования потоков данных эффективно.

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

Нетерминальные операции преобразуют один поток в другой, возвращая новый Stream. Они ленивы — не выполняются до тех пор, пока не будет вызвана терминальная операция.

Основные нетерминальные операции:

1. map() — преобразование элементов

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

List<Integer> squared = numbers.stream()
    .map(n -> n * n)           // Преобразование 1→1, 2→4, 3→9...
    .collect(Collectors.toList()); // Терминальная операция

System.out.println(squared);   // [1, 4, 9, 16, 25]

2. filter() — фильтрация элементов

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Anna");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A")) // Оставляем только начинающиеся на "A"
    .collect(Collectors.toList());

System.out.println(aNames);    // [Alice, Anna]

3. flatMap() — развёртывание потоков

List<List<Integer>> lists = Arrays.asList(
    Arrays.asList(1, 2),
    Arrays.asList(3, 4),
    Arrays.asList(5, 6)
);

List<Integer> flattened = lists.stream()
    .flatMap(list -> list.stream()) // Превращаем List<List<>> в Stream<>
    .collect(Collectors.toList());

System.out.println(flattened);  // [1, 2, 3, 4, 5, 6]

4. distinct() — удаление дубликатов

List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4);

List<Integer> unique = numbers.stream()
    .distinct()                // Оставляем только уникальные
    .collect(Collectors.toList());

System.out.println(unique);    // [1, 2, 3, 4]

5. sorted() — сортировка

List<String> words = Arrays.asList("zebra", "apple", "banana");

List<String> sorted = words.stream()
    .sorted()                  // Сортирует в естественном порядке
    .collect(Collectors.toList());

System.out.println(sorted);    // [apple, banana, zebra]

// С компаратором
List<String> reverseSorted = words.stream()
    .sorted(Comparator.reverseOrder())
    .collect(Collectors.toList());

System.out.println(reverseSorted); // [zebra, banana, apple]

6. limit() и skip() — ограничение и пропуск

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

// Первые 3 элемента
List<Integer> first3 = numbers.stream()
    .limit(3)
    .collect(Collectors.toList());
System.out.println(first3);     // [1, 2, 3]

// Пропустить первые 2, взять 3
List<Integer> page = numbers.stream()
    .skip(2)                   // Пропускаем первые 2
    .limit(3)                  // Берём следующие 3
    .collect(Collectors.toList());
System.out.println(page);       // [3, 4, 5]

7. peek() — отладка

List<Integer> numbers = Arrays.asList(1, 2, 3);

List<Integer> result = numbers.stream()
    .map(n -> n * 2)
    .peek(n -> System.out.println("После map: " + n))  // Только для отладки
    .filter(n -> n > 2)
    .peek(n -> System.out.println("После filter: " + n))
    .collect(Collectors.toList());

System.out.println(result);     // [4, 6]

// Вывод отладки:
// После map: 2
// После map: 4
// После filter: 4
// После map: 6
// После filter: 6

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

Терминальные операции заканчивают конвейер обработки и возвращают финальный результат (не Stream). Они немедленно выполняют всю цепочку нетерминальных операций.

Основные терминальные операции:

1. collect() — собрать в коллекцию

List<String> words = Arrays.asList("apple", "banana", "cherry");

// В List
List<String> list = words.stream()
    .filter(w -> w.length() > 5)
    .collect(Collectors.toList());

// В Set
Set<String> set = words.stream()
    .collect(Collectors.toSet());

// В String (конкатенация)
String joined = words.stream()
    .collect(Collectors.joining(", "));
System.out.println(joined);     // apple, banana, cherry

// В Map
Map<String, Integer> wordLengths = words.stream()
    .collect(Collectors.toMap(
        word -> word,
        String::length
    ));

2. forEach() — для каждого элемента

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

names.stream()
    .forEach(name -> System.out.println("Hello, " + name));

// Вывод:
// Hello, Alice
// Hello, Bob
// Hello, Charlie

3. reduce() — сведение к одному значению

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Сумма
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);
System.out.println(sum);        // 15

// Произведение
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product);    // 120

// С Optional (без начального значения)
Optional<Integer> max = numbers.stream()
    .reduce((a, b) -> a > b ? a : b);
max.ifPresent(System.out::println); // 5

4. count() — количество элементов

List<String> words = Arrays.asList("apple", "banana", "cherry", "date");

long count = words.stream()
    .filter(w -> w.length() > 4)
    .count();
System.out.println(count);      // 3 (apple, banana, cherry)

5. anyMatch(), allMatch(), noneMatch() — проверка условия

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Есть ли чётные?
boolean hasEven = numbers.stream()
    .anyMatch(n -> n % 2 == 0);
System.out.println(hasEven);    // true

// Все ли > 0?
boolean allPositive = numbers.stream()
    .allMatch(n -> n > 0);
System.out.println(allPositive); // true

// Нет ли > 10?
boolean noneGreaterThan10 = numbers.stream()
    .noneMatch(n -> n > 10);
System.out.println(noneGreaterThan10); // true

6. findFirst() и findAny() — найти первый/любой

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Первый чётный
Optional<Integer> first = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();
first.ifPresent(System.out::println); // 2

// Любой элемент (может отличаться в многопоточности)
Optional<Integer> any = numbers.stream()
    .findAny();
any.ifPresent(System.out::println);

7. min() и max() — минимум и максимум

List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);

Optional<Integer> min = numbers.stream()
    .min(Comparator.naturalOrder());
min.ifPresent(System.out::println); // 1

Optional<Integer> max = numbers.stream()
    .max(Comparator.naturalOrder());
max.ifPresent(System.out::println); // 9

Сравнение терминальных и нетерминальных

ХарактеристикаНетерминальныеТерминальные
Возвращаемый типStreamЗначение, Optional, void
ВыполнениеЛенивое (отложенное)Немедленное
РезультатПромежуточный потокФинальный результат
Можно цепировать✓ Да✗ Нет (конец цепи)
Примерыmap, filter, sortedcollect, forEach, count

Ленивое вычисление (Lazy Evaluation)

Это ключевая особенность нетерминальных операций:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// БЕЗ терминальной операции - ничего не выполняется
Stream<Integer> stream = numbers.stream()
    .map(n -> {
        System.out.println("Вычисляю: " + n);
        return n * 2;
    })
    .filter(n -> {
        System.out.println("Фильтрую: " + n);
        return n > 4;
    });

// Ничего не выведется! Операции не выполнены

// С терминальной операцией - выполняется цепочка
List<Integer> result = numbers.stream()
    .map(n -> {
        System.out.println("Вычисляю: " + n);
        return n * 2;
    })
    .filter(n -> {
        System.out.println("Фильтрую: " + n);
        return n > 4;
    })
    .collect(Collectors.toList()); // ← Терминальная операция

// Вывод (обработка идёт по элементам, не по операциям):
// Вычисляю: 1
// Фильтрую: 2
// Вычисляю: 2
// Фильтрую: 4
// Вычисляю: 3
// Фильтрую: 6
// Вычисляю: 4
// Фильтрую: 8
// Вычисляю: 5
// Фильтрую: 10

System.out.println(result);     // [6, 8, 10]

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

public class StreamExample {
    static class User {
        String name;
        int age;
        List<String> skills;

        public User(String name, int age, List<String> skills) {
            this.name = name;
            this.age = age;
            this.skills = skills;
        }
    }

    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("Alice", 25, Arrays.asList("Java", "SQL")),
            new User("Bob", 32, Arrays.asList("Python", "ML")),
            new User("Charlie", 22, Arrays.asList("Java", "JavaScript")),
            new User("Diana", 28, Arrays.asList("Go", "Rust"))
        );

        // Найти всех пользователей 25+, с Java навыком, отсортировать по имени
        List<String> result = users.stream()
            // Нетерминальная: filter
            .filter(user -> user.age >= 25)
            // Нетерминальная: filter
            .filter(user -> user.skills.contains("Java"))
            // Нетерминальная: map
            .map(user -> user.name)
            // Нетерминальная: sorted
            .sorted()
            // Терминальная: collect
            .collect(Collectors.toList());

        System.out.println(result);     // [Alice, Charlie]
    }
}

Заключение

Нетерминальные операции (map, filter, sorted, etc.):

  • Возвращают Stream
  • Ленивое вычисление (пока нет терминала)
  • Можно цепировать

Терминальные операции (collect, forEach, count, etc.):

  • Возвращают финальный результат
  • Немедленное выполнение
  • Конец цепочки

Помни: без терминальной операции нетерминальные операции вообще не выполняются!

В чём разница между терминальными и нетерминальными (промежуточными) операциями в Stream API? | PrepBro