Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

collect(), forEach(), reduce(), count(), findFirst(), anyMatch()

Материалы

ТипСсылка
ДокументStream Operations
Видеоссылка

Что такое терминальные операции?

Терминальные операции (terminal operations) - это операции, которые:

  • Запускают выполнение всего pipeline
  • Возвращают результат (не Stream)
  • После выполнения Stream нельзя использовать повторно
Источник → filter() → map() → [терминальная операция] → Результат
                               ↑
                         Запускает весь pipeline

Классификация терминальных операций

ТипОперацииВозвращает
Собирающиеcollect(), toArray(), toList()Коллекция/массив
ИтерирующиеforEach(), forEachOrdered()void
Редуцирующиеreduce(), count(), sum(), min(), max()Значение
ПоисковыеfindFirst(), findAny()Optional
ПроверяющиеanyMatch(), allMatch(), noneMatch()boolean

forEach() - Итерация по элементам

Выполняет действие для каждого элемента.

Сигнатура

void forEach(Consumer<? super T> action)
void forEachOrdered(Consumer<? super T> action)  // гарантирует порядок

Базовые примеры

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

// Вывод каждого элемента
names.stream()
    .forEach(System.out::println);

// С лямбдой
names.stream()
    .forEach(name -> System.out.println("Hello, " + name));

// После обработки
names.stream()
    .filter(n -> n.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);  // ALICE, CHARLIE

forEach vs forEachOrdered

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

// Параллельный stream - порядок НЕ гарантирован
numbers.parallelStream()
    .forEach(System.out::println);  // Может быть: 3, 1, 5, 2, 4

// Параллельный stream - порядок ГАРАНТИРОВАН
numbers.parallelStream()
    .forEachOrdered(System.out::println);  // Всегда: 1, 2, 3, 4, 5

Побочные эффекты в forEach

// Допустимо - вывод, логирование
names.stream()
    .forEach(name -> logger.info("Processing: " + name));

// Плохо - модификация внешних структур
List<String> result = new ArrayList<>();
names.stream()
    .forEach(result::add);  // Работает, но лучше collect()

// Хорошо
List<String> result = names.stream()
    .collect(Collectors.toList());

forEach на Map

Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);

ages.forEach((name, age) ->
    System.out.println(name + " is " + age + " years old")
);

collect() - Сборка результата

Собирает элементы в коллекцию или другую структуру.

Сигнатуры

<R> R collect(Supplier<R> supplier,
              BiConsumer<R, ? super T> accumulator,
              BiConsumer<R, R> combiner)

<R, A> R collect(Collector<? super T, A, R> collector)

Базовые коллекторы

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

// В List
List<String> list = names.stream()
    .filter(n -> n.length() > 3)
    .collect(Collectors.toList());

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

// toList() (Java 16+) - неизменяемый список
List<String> immutableList = names.stream()
    .toList();

toArray() - В массив

// В Object[]
Object[] array = names.stream().toArray();

// В String[]
String[] stringArray = names.stream()
    .toArray(String[]::new);

// В Integer[] (с преобразованием)
Integer[] numbers = Stream.of("1", "2", "3")
    .map(Integer::parseInt)
    .toArray(Integer[]::new);

joining() - Объединение строк

List<String> words = List.of("Hello", "World", "Java");

// Простое объединение
String joined = words.stream()
    .collect(Collectors.joining());  // "HelloWorldJava"

// С разделителем
String csv = words.stream()
    .collect(Collectors.joining(", "));  // "Hello, World, Java"

// С разделителем, префиксом и суффиксом
String json = words.stream()
    .collect(Collectors.joining(", ", "[", "]"));  // "[Hello, World, Java]"

Подробнее о Collectors - см. раздел 2.3.4


reduce() - Свертка

Сводит все элементы к одному значению.

Сигнатуры

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

Визуализация reduce

[1, 2, 3, 4, 5]
    ↓ reduce((a, b) -> a + b)

1 + 2 = 3
    3 + 3 = 6
        6 + 4 = 10
            10 + 5 = 15

Результат: 15

reduce без identity (возвращает Optional)

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

// Сумма
Optional<Integer> sum = numbers.stream()
    .reduce((a, b) -> a + b);
sum.ifPresent(System.out::println);  // 15

// Или с method reference
Optional<Integer> sum = numbers.stream()
    .reduce(Integer::sum);

// Максимум
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);  // 5

// Конкатенация строк
List<String> words = List.of("a", "b", "c");
Optional<String> concat = words.stream()
    .reduce((a, b) -> a + b);  // "abc"

reduce с identity (возвращает значение)

// Сумма с начальным значением 0
int sum = numbers.stream()
    .reduce(0, Integer::sum);  // 15

// Произведение с начальным значением 1
int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);  // 120

// Пустой stream - вернет identity
int sumEmpty = Stream.<Integer>empty()
    .reduce(0, Integer::sum);  // 0

Выбор identity

// Сумма: identity = 0 (a + 0 = a)
reduce(0, Integer::sum)

// Произведение: identity = 1 (a * 1 = a)
reduce(1, (a, b) -> a * b)

// Максимум: identity = Integer.MIN_VALUE
reduce(Integer.MIN_VALUE, Integer::max)

// Конкатенация: identity = ""
reduce("", String::concat)

reduce с преобразованием типа

// Сумма длин строк
List<String> words = List.of("Hello", "World");

int totalLength = words.stream()
    .reduce(0,                          // identity
            (sum, word) -> sum + word.length(),  // accumulator
            Integer::sum);              // combiner (для parallel)
// 10

reduce vs специализированные методы

// reduce для суммы
int sum1 = numbers.stream()
    .reduce(0, Integer::sum);

// Лучше: mapToInt + sum
int sum2 = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();

// reduce для максимума
Optional<Integer> max1 = numbers.stream()
    .reduce(Integer::max);

// Лучше: max() с компаратором
Optional<Integer> max2 = numbers.stream()
    .max(Integer::compareTo);

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

record Product(String name, double price) {}

List<Product> products = List.of(
    new Product("Book", 20),
    new Product("Laptop", 1000),
    new Product("Phone", 500)
);

// Общая стоимость
double total = products.stream()
    .map(Product::price)
    .reduce(0.0, Double::sum);  // 1520.0

// Самый дорогой товар
Optional<Product> mostExpensive = products.stream()
    .reduce((p1, p2) -> p1.price() > p2.price() ? p1 : p2);

// Или через max()
Optional<Product> mostExpensive = products.stream()
    .max(Comparator.comparing(Product::price));

count() - Подсчет элементов

Возвращает количество элементов.

Сигнатура

long count()

Примеры

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

// Общее количество
long total = names.stream().count();  // 4

// Количество после фильтрации
long startsWithA = names.stream()
    .filter(n -> n.startsWith("A"))
    .count();  // 1

// Количество уникальных
long unique = names.stream()
    .distinct()
    .count();

Оптимизация count()

// С Java 9+ count() оптимизирован
// Не итерирует элементы если размер известен

List<String> list = List.of("a", "b", "c");
long count = list.stream().count();  // O(1), не O(n)

// Но после операций - итерация нужна
long count = list.stream()
    .filter(s -> s.length() > 0)
    .count();  // O(n)

min() и max() - Поиск экстремумов

Находят минимальный/максимальный элемент.

Сигнатуры

Optional<T> min(Comparator<? super T> comparator)
Optional<T> max(Comparator<? super T> comparator)

Базовые примеры

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

// Минимум
Optional<Integer> min = numbers.stream()
    .min(Integer::compareTo);  // 1

// Максимум
Optional<Integer> max = numbers.stream()
    .max(Integer::compareTo);  // 9

// Для Comparable типов
Optional<String> first = List.of("Charlie", "Alice", "Bob").stream()
    .min(Comparator.naturalOrder());  // "Alice"

min/max для объектов

record Person(String name, int age) {}

List<Person> people = List.of(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Charlie", 35)
);

// Самый молодой
Optional<Person> youngest = people.stream()
    .min(Comparator.comparing(Person::age));  // Bob, 25

// Самый старший
Optional<Person> oldest = people.stream()
    .max(Comparator.comparing(Person::age));  // Charlie, 35

// По имени (алфавитно последний)
Optional<Person> lastByName = people.stream()
    .max(Comparator.comparing(Person::name));  // Charlie

IntStream/LongStream/DoubleStream min/max

// Примитивные стримы - без компаратора
OptionalInt min = IntStream.of(5, 2, 8, 1, 9).min();  // 1
OptionalLong max = LongStream.of(100L, 200L, 300L).max();  // 300
OptionalDouble minDouble = DoubleStream.of(1.5, 2.5, 0.5).min();  // 0.5

findFirst() и findAny() - Поиск элемента

Находят первый или любой элемент.

Сигнатуры

Optional<T> findFirst()
Optional<T> findAny()

Разница между findFirst и findAny

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

// findFirst - всегда первый элемент
Optional<Integer> first = numbers.stream()
    .filter(n -> n > 2)
    .findFirst();  // 3 (всегда)

// findAny - любой элемент (быстрее в parallel)
Optional<Integer> any = numbers.parallelStream()
    .filter(n -> n > 2)
    .findAny();  // 3, 4 или 5 (недетерминировано)

Когда использовать findAny

// Если порядок не важен + parallel stream
boolean hasEven = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .findAny()
    .isPresent();

// Если порядок важен
Optional<Integer> firstEven = numbers.stream()
    .filter(n -> n % 2 == 0)
    .findFirst();

Short-circuiting

// findFirst/findAny - short-circuiting
// Останавливаются при первом совпадении

Stream.iterate(1, n -> n + 1)  // Бесконечный поток
    .filter(n -> n > 100)
    .findFirst();  // 101 (не зависает!)

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

record User(String name, String email, boolean active) {}

List<User> users = ...;

// Найти первого активного пользователя
Optional<User> firstActive = users.stream()
    .filter(User::active)
    .findFirst();

// Найти пользователя по email
Optional<User> byEmail = users.stream()
    .filter(u -> u.email().equals("alice@example.com"))
    .findFirst();

// С orElse
User user = users.stream()
    .filter(u -> u.name().equals("Alice"))
    .findFirst()
    .orElse(new User("Guest", "guest@example.com", false));

// С orElseThrow
User user = users.stream()
    .filter(u -> u.name().equals("Alice"))
    .findFirst()
    .orElseThrow(() -> new UserNotFoundException("Alice"));

anyMatch(), allMatch(), noneMatch() - Проверки

Проверяют соответствие элементов условию.

Сигнатуры

boolean anyMatch(Predicate<? super T> predicate)   // хотя бы один
boolean allMatch(Predicate<? super T> predicate)   // все
boolean noneMatch(Predicate<? super T> predicate)  // ни один

anyMatch - хотя бы один

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

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

// Есть ли больше 10?
boolean hasLarge = numbers.stream()
    .anyMatch(n -> n > 10);  // false

allMatch - все элементы

List<Integer> numbers = List.of(2, 4, 6, 8);

// Все четные?
boolean allEven = numbers.stream()
    .allMatch(n -> n % 2 == 0);  // true

// Все положительные?
boolean allPositive = numbers.stream()
    .allMatch(n -> n > 0);  // true

// Внимание: пустой stream
boolean emptyCheck = Stream.<Integer>empty()
    .allMatch(n -> n > 100);  // true (!)

noneMatch - ни один элемент

List<Integer> numbers = List.of(1, 3, 5, 7);

// Нет четных?
boolean noEven = numbers.stream()
    .noneMatch(n -> n % 2 == 0);  // true

// Нет отрицательных?
boolean noNegative = numbers.stream()
    .noneMatch(n -> n < 0);  // true

Short-circuiting поведение

// anyMatch - останавливается при первом true
Stream.of(1, 2, 3, 4, 5)
    .peek(System.out::println)
    .anyMatch(n -> n == 3);
// Выведет: 1, 2, 3 (дальше не идет)

// allMatch - останавливается при первом false
Stream.of(2, 4, 5, 6, 8)
    .peek(System.out::println)
    .allMatch(n -> n % 2 == 0);
// Выведет: 2, 4, 5 (5 - нечетное, останавливается)

// noneMatch - останавливается при первом true
Stream.of(1, 3, 4, 5, 7)
    .peek(System.out::println)
    .noneMatch(n -> n % 2 == 0);
// Выведет: 1, 3, 4 (4 - четное, останавливается)

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

record Product(String name, double price, boolean inStock) {}

List<Product> products = ...;

// Все товары в наличии?
boolean allInStock = products.stream()
    .allMatch(Product::inStock);

// Есть дорогие товары (> 1000)?
boolean hasExpensive = products.stream()
    .anyMatch(p -> p.price() > 1000);

// Нет бесплатных товаров?
boolean noFree = products.stream()
    .noneMatch(p -> p.price() == 0);

// Валидация
boolean allValid = users.stream()
    .allMatch(u -> u.email() != null && u.email().contains("@"));

Взаимосвязь операций

// noneMatch(predicate) эквивалентно !anyMatch(predicate)
// allMatch(predicate) эквивалентно noneMatch(predicate.negate())

Predicate<Integer> isEven = n -> n % 2 == 0;

boolean result1 = numbers.stream().noneMatch(isEven);
boolean result2 = !numbers.stream().anyMatch(isEven);
// result1 == result2

Примитивные стримы: sum(), average(), summaryStatistics()

sum()

int[] numbers = {1, 2, 3, 4, 5};

int sum = Arrays.stream(numbers).sum();  // 15

// Для Stream<Integer> - нужен mapToInt
List<Integer> list = List.of(1, 2, 3, 4, 5);
int sum = list.stream()
    .mapToInt(Integer::intValue)
    .sum();  // 15

average()

OptionalDouble avg = IntStream.of(1, 2, 3, 4, 5)
    .average();  // 3.0

double average = avg.orElse(0.0);

// Пустой стрим
OptionalDouble empty = IntStream.empty().average();  // OptionalDouble.empty

summaryStatistics()

IntSummaryStatistics stats = IntStream.of(1, 2, 3, 4, 5)
    .summaryStatistics();

stats.getCount();    // 5
stats.getSum();      // 15
stats.getMin();      // 1
stats.getMax();      // 5
stats.getAverage();  // 3.0

// Для объектов
DoubleSummaryStatistics priceStats = products.stream()
    .mapToDouble(Product::price)
    .summaryStatistics();

iterator() и spliterator()

iterator() - Получить Iterator

Stream<String> stream = Stream.of("a", "b", "c");
Iterator<String> iterator = stream.iterator();

while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

// Внимание: Stream после этого нельзя использовать

spliterator() - Для параллельной обработки

Stream<String> stream = Stream.of("a", "b", "c");
Spliterator<String> spliterator = stream.spliterator();

// Характеристики
spliterator.characteristics();  // ORDERED, SIZED, etc.
spliterator.estimateSize();     // Примерное количество элементов

Обработка результатов Optional

Основные методы Optional

Optional<String> result = names.stream()
    .filter(n -> n.startsWith("X"))
    .findFirst();

// Проверка наличия
if (result.isPresent()) {
    System.out.println(result.get());
}

// orElse - значение по умолчанию
String name = result.orElse("Unknown");

// orElseGet - ленивое вычисление
String name = result.orElseGet(() -> computeDefault());

// orElseThrow - бросить исключение
String name = result.orElseThrow(() -> new NoSuchElementException());

// ifPresent - выполнить действие
result.ifPresent(System.out::println);

// ifPresentOrElse (Java 9+)
result.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("Not found")
);

Цепочки с Optional

Optional<String> email = users.stream()
    .filter(u -> u.name().equals("Alice"))
    .findFirst()
    .map(User::email)               // Optional<String>
    .filter(e -> e.contains("@"))   // Optional<String>
    .map(String::toLowerCase);      // Optional<String>

Порядок терминальных операций

Ленивое выполнение

// Ничего не выполняется - нет терминальной операции
Stream<String> stream = names.stream()
    .filter(n -> {
        System.out.println("Filtering: " + n);
        return n.length() > 3;
    })
    .map(n -> {
        System.out.println("Mapping: " + n);
        return n.toUpperCase();
    });

// Теперь выполняется - есть терминальная операция
stream.forEach(System.out::println);

Порядок обработки

List.of("Alice", "Bob", "Charlie").stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n.length() > 3;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n.toUpperCase();
    })
    .forEach(n -> System.out.println("forEach: " + n));

// Вывод (элемент за элементом, не операция за операцией):
// filter: Alice
// map: Alice
// forEach: ALICE
// filter: Bob
// filter: Charlie
// map: Charlie
// forEach: CHARLIE

Сводная таблица терминальных операций

ОперацияОписаниеВозвращаетShort-circuit
forEach()Действие для каждогоvoidНет
forEachOrdered()forEach с гарантией порядкаvoidНет
collect()Сборка в коллекциюRНет
toArray()Сборка в массивObject[]/T[]Нет
toList()Сборка в ListListНет
reduce()Свертка в одно значениеOptional/TНет
count()Количество элементовlongНет
min()Минимальный элементOptionalНет
max()Максимальный элементOptionalНет
findFirst()Первый элементOptionalДа
findAny()Любой элементOptionalДа
anyMatch()Есть ли совпадениеbooleanДа
allMatch()Все ли совпадаютbooleanДа
noneMatch()Нет ли совпаденийbooleanДа
sum()Сумма (примитивы)int/long/doubleНет
average()Среднее (примитивы)OptionalDoubleНет

Итоги

  1. Терминальные операции запускают pipeline - без них промежуточные операции не выполняются
  2. forEach - для побочных эффектов (вывод, логирование)
  3. collect - для сборки результата в коллекцию
  4. reduce - для свертки в одно значение
  5. findFirst/findAny - для поиска элемента (short-circuit)
  6. anyMatch/allMatch/noneMatch - для проверок (short-circuit)
  7. count/min/max/sum/average - для агрегации
  8. Stream одноразовый - после терминальной операции использовать нельзя
  9. Результат часто Optional - обрабатывай отсутствие значения

Задания для практики

  1. reduce: Дан список чисел. Найди произведение всех положительных чисел с помощью reduce.

  2. findFirst + filter: Дан список пользователей User(name, email, age). Найди первого совершеннолетнего (age >= 18) пользователя с email на gmail.com.

  3. allMatch/anyMatch: Дан список заказов Order(items, status). Проверь: а) все ли заказы доставлены, б) есть ли отмененные заказы.

  4. collect + reduce: Дан список транзакций Transaction(type, amount). Посчитай баланс (сумма DEPOSIT минус сумма WITHDRAW).

  5. Комплексная задача: Дан список студентов Student(name, grades: List<Integer>). Найди студента с наивысшим средним баллом. Если таких несколько - любого из них.