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

В какой момент выполняются операции в Stream

1.0 Junior🔥 161 комментариев
#Stream API и функциональное программирование#Основы Java

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

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

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

Когда выполняются операции в Stream

Одна из самых важных концепций в Java Stream API — это разделение операций на промежуточные (intermediate) и терминальные (terminal). Это разделение определяет, когда и как выполняются операции в потоке данных.

Основной принцип: ленивое вычисление (Lazy Evaluation)

Ключевой особенность Stream API является ленивое вычисление. Промежуточные операции не выполняются до момента вызова терминальной операции.

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

// Эти операции НЕ выполняются сразу!
Stream<Integer> stream = numbers.stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n > 3;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * 2;
    });

// На этом моменте НИЧЕГО не выведется!
// Stream — это просто описание операций

// Терминальная операция ЗАПУСКАЕТ выполнение
List<Integer> result = stream.collect(Collectors.toList());

// ТЕПЕРЬ выполнятся все операции
// Вывод:
// filter: 1
// filter: 2
// filter: 3
// filter: 4
// map: 4
// filter: 5
// map: 5
// и т.д.

Промежуточные операции (Intermediate Operations)

Промежуточные операции возвращают новый Stream и НЕ выполняются сразу. Они создают описание преобразования, которое будет применено позже.

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

Stream<String> stream = words.stream()
    .filter(w -> {
        System.out.println("filter called");
        return w.startsWith("a");
    })
    .map(w -> {
        System.out.println("map called");
        return w.toUpperCase();
    })
    .limit(2);

// НИЧЕГО не выполнилось!
System.out.println("Stream создан, но не выполнен");

// Теперь выполняется
stream.forEach(System.out::println);
// Вывод:
// filter called
// map called
// APPLE
// filter called
// map called
// APRICOT

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

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

// 1. filter — фильтрует элементы
stream.filter(n -> n > 5);

// 2. map — преобразует каждый элемент
stream.map(n -> n * 2);

// 3. flatMap — преобразует в поток и объединяет
stream.flatMap(n -> Arrays.stream(new Integer[]{n, n*2}));

// 4. distinct — удаляет дубликаты
stream.distinct();

// 5. sorted — сортирует
stream.sorted();
stream.sorted(Comparator.reverseOrder());

// 6. limit — берёт первые N элементов
stream.limit(5);

// 7. skip — пропускает первые N элементов
stream.skip(3);

// 8. peek — выполняет операцию для каждого элемента (для отладки)
stream.peek(System.out::println);

// 9. takeWhile (Java 9+) — берёт элементы пока условие истинно
stream.takeWhile(n -> n < 7);

// 10. dropWhile (Java 9+) — пропускает элементы пока условие истинно
stream.dropWhile(n -> n < 3);

Терминальные операции (Terminal Operations)

Терминальные операции ЗАПУСКАЮТ выполнение всей цепочки операций и возвращают результат (не Stream).

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

// 1. forEach — выполняет операцию для каждого элемента
numbers.stream()
    .filter(n -> n > 2)
    .forEach(System.out::println); // ВЫПОЛНЯЕТСЯ

// 2. collect — собирает результаты
List<Integer> result = numbers.stream()
    .filter(n -> n > 2)
    .collect(Collectors.toList()); // ВЫПОЛНЯЕТСЯ

// 3. toArray — преобразует в массив
Integer[] array = numbers.stream()
    .filter(n -> n > 2)
    .toArray(Integer[]::new); // ВЫПОЛНЯЕТСЯ

// 4. reduce — агрегирует элементы
Optional<Integer> sum = numbers.stream()
    .reduce(Integer::sum); // ВЫПОЛНЯЕТСЯ

int total = numbers.stream()
    .reduce(0, Integer::sum); // ВЫПОЛНЯЕТСЯ

// 5. count — подсчитывает количество
long count = numbers.stream()
    .filter(n -> n > 2)
    .count(); // ВЫПОЛНЯЕТСЯ

// 6. findFirst — находит первый элемент
Optional<Integer> first = numbers.stream()
    .filter(n -> n > 2)
    .findFirst(); // ВЫПОЛНЯЕТСЯ

// 7. findAny — находит любой элемент
Optional<Integer> any = numbers.stream()
    .filter(n -> n > 2)
    .findAny(); // ВЫПОЛНЯЕТСЯ

// 8. anyMatch — проверяет наличие элемента
boolean exists = numbers.stream()
    .anyMatch(n -> n > 4); // ВЫПОЛНЯЕТСЯ

// 9. allMatch — проверяет все элементы
boolean all = numbers.stream()
    .allMatch(n -> n > 0); // ВЫПОЛНЯЕТСЯ

// 10. noneMatch — проверяет отсутствие элемента
boolean none = numbers.stream()
    .noneMatch(n -> n > 10); // ВЫПОЛНЯЕТСЯ

// 11. min/max — находит минимум/максимум
Optional<Integer> min = numbers.stream()
    .min(Integer::compareTo); // ВЫПОЛНЯЕТСЯ

// 12. sum, average (для специализированных потоков)
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum(); // ВЫПОЛНЯЕТСЯ

Порядок выполнения: вертикальный обход

Операции в Stream применяются вертикально (по элементам), а не горизонтально (по операциям).

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

numbers.stream()
    .filter(n -> n > 3)      // Шаг 1
    .map(n -> n * 2)         // Шаг 2
    .filter(n -> n < 15)     // Шаг 3
    .forEach(System.out::println); // Шаг 4: ВЫПОЛНИТЬ

// Порядок выполнения для КАЖДОГО элемента:
// Элемент 1: filter(1 > 3) -> FALSE -> пропущен
// Элемент 2: filter(2 > 3) -> FALSE -> пропущен
// Элемент 3: filter(3 > 3) -> FALSE -> пропущен
// Элемент 4: filter(4 > 3) -> TRUE -> map(4*2=8) -> filter(8 < 15) -> TRUE -> печать 8
// Элемент 5: filter(5 > 3) -> TRUE -> map(5*2=10) -> filter(10 < 15) -> TRUE -> печать 10
// Элемент 6: filter(6 > 3) -> TRUE -> map(6*2=12) -> filter(12 < 15) -> TRUE -> печать 12
// Элемент 7: filter(7 > 3) -> TRUE -> map(7*2=14) -> filter(14 < 15) -> TRUE -> печать 14
// Элемент 8: filter(8 > 3) -> TRUE -> map(8*2=16) -> filter(16 < 15) -> FALSE -> пропущен
// И т.д.

// Вывод: 8, 10, 12, 14

Важность limit для производительности

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

// ПРИМЕР 1: Без limit
numbers.stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n > 3;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * 2;
    })
    .forEach(System.out::println);
// Обработаны ВСЕ элементы

// ПРИМЕР 2: С limit
numbers.stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n > 3;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * 2;
    })
    .limit(2)  // Ограничиваем результат
    .forEach(System.out::println);
// Обработаны только ПЕРВЫЕ 2 подходящих элемента!
// filter: 1
// filter: 2
// filter: 3
// filter: 4
// map: 4
// 8
// filter: 5
// map: 5
// 10
// (останавливается, так как получены 2 элемента)

Практический пример

public class StreamExecutionExample {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User(1, "Alice", 30),
            new User(2, "Bob", 25),
            new User(3, "Charlie", 35),
            new User(4, "Diana", 28),
            new User(5, "Eve", 32)
        );
        
        // Ленивое выполнение
        Stream<String> names = users.stream()
            .filter(u -> {
                System.out.println("Filtering: " + u.getName());
                return u.getAge() > 26;
            })
            .map(u -> {
                System.out.println("Mapping: " + u.getName());
                return u.getName().toUpperCase();
            })
            .limit(2);
        
        System.out.println("Stream created, nothing executed yet\n");
        
        // Терминальная операция ЗАПУСКАЕТ выполнение
        System.out.println("Starting terminal operation:\n");
        names.forEach(name -> System.out.println("Result: " + name));
    }
}

class User {
    private int id;
    private String name;
    private int age;
    
    public User(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
}

// Вывод:
// Stream created, nothing executed yet
// 
// Starting terminal operation:
// 
// Filtering: Alice
// Filtering: Bob
// Filtering: Charlie
// Mapping: Charlie
// Result: CHARLIE
// Filtering: Diana
// Mapping: Diana
// Result: DIANA

Стадии выполнения

1. Создание Stream: numbers.stream()
   ↓
2. Промежуточные операции: .filter(...).map(...).limit(2)
   ↓ (Ничего не выполняется!)
3. Терминальная операция: .forEach(...)
   ↓
4. ВЫПОЛНЕНИЕ всей цепочки в правильном порядке

Заключение

Понимание ленивого вычисления в Stream API критично для:

  • Производительности — операции не выполняются зря
  • Бесконечных потоков — можно работать с бесконечными потоками благодаря ленивости
  • Короткозамыкания — операции вроде findFirst() могут остановиться рано

Запомни: Промежуточные операции — это просто описание, терминальные операции — это команда к выполнению.