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() | Сборка в List | List | Нет |
reduce() | Свертка в одно значение | Optional | Нет |
count() | Количество элементов | long | Нет |
min() | Минимальный элемент | Optional | Нет |
max() | Максимальный элемент | Optional | Нет |
findFirst() | Первый элемент | Optional | Да |
findAny() | Любой элемент | Optional | Да |
anyMatch() | Есть ли совпадение | boolean | Да |
allMatch() | Все ли совпадают | boolean | Да |
noneMatch() | Нет ли совпадений | boolean | Да |
sum() | Сумма (примитивы) | int/long/double | Нет |
average() | Среднее (примитивы) | OptionalDouble | Нет |
Итоги
- Терминальные операции запускают pipeline - без них промежуточные операции не выполняются
- forEach - для побочных эффектов (вывод, логирование)
- collect - для сборки результата в коллекцию
- reduce - для свертки в одно значение
- findFirst/findAny - для поиска элемента (short-circuit)
- anyMatch/allMatch/noneMatch - для проверок (short-circuit)
- count/min/max/sum/average - для агрегации
- Stream одноразовый - после терминальной операции использовать нельзя
- Результат часто Optional - обрабатывай отсутствие значения
Задания для практики
-
reduce: Дан список чисел. Найди произведение всех положительных чисел с помощью reduce.
-
findFirst + filter: Дан список пользователей
User(name, email, age). Найди первого совершеннолетнего (age >= 18) пользователя с email на gmail.com. -
allMatch/anyMatch: Дан список заказов
Order(items, status). Проверь: а) все ли заказы доставлены, б) есть ли отмененные заказы. -
collect + reduce: Дан список транзакций
Transaction(type, amount). Посчитай баланс (сумма DEPOSIT минус сумма WITHDRAW). -
Комплексная задача: Дан список студентов
Student(name, grades: List<Integer>). Найди студента с наивысшим средним баллом. Если таких несколько - любого из них.