Stream API: filter, map, collect
До сих пор каждая станция работала сама по себе: removeIf удалял, forEach печатал, sort сортировал. Теперь соберём конвейер целиком.
Коробки приезжают со склада. На первой станции лишние снимают с ленты, а на второй каждую оставшуюся перекладывают в новую упаковку. В конце сборщик складывает всё, что доехало, в ящик. Склад при этом не меняется: конвейер работает с коробками на ленте, а не с полками склада. В Java такой конвейер строят из стрима:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("cat", "zebra", "ox", "horse");
List<String> result = words.stream()
.filter(w -> w.length() > 2)
.map(w -> w.toUpperCase())
.toList();
System.out.println(result); // [CAT, ZEBRA, HORSE]
System.out.println(words); // [cat, zebra, ox, horse]
}
}Давайте разберём программу по шагам:
words.stream()выкладывает слова списка на ленту — создаёт стрим.filter(w -> w.length() > 2)— станция, которая пропускает дальше только слова длиннее двух букв. Словоoxснимается с ленты.map(w -> w.toUpperCase())— станция, которая заменяет каждое слово новым, написанным заглавными буквами.toList()— сборщик в конце: он складывает всё, что доехало, в новый список.- Первая печать даёт
[CAT, ZEBRA, HORSE], а вторая показывает, чтоwordsне изменился.
Что такое стрим
Стрим — конвейер, по которому едут элементы источника, например списка. Стрим не хранит элементы и не меняет источник: filter не удаляет ox из words, а только не пускает его дальше по ленте.
Карточки станций вам уже знакомы. filter ждёт Predicate и пропускает элемент, если для него Predicate вернул true. map ждёт Function и ставит на ленту вместо элемента результат этой Function.
Здесь картинка с конвейером неточна. На настоящей станции работник перекладывает ту же коробку в новую упаковку. map же выпускает на ленту новый объект, а исходная строка остаётся какой была.
Промежуточные и терминальные операции
Методы стрима бывают двух видов, и на собеседовании часто просят их различить.
Промежуточные
Промежуточная операция — станция на ленте. Она возвращает новый стрим, поэтому за ней можно поставить следующую. filter и map — промежуточные операции.
Терминальные
Терминальная операция — сборщик в конце ленты. Она возвращает уже не стрим, а результат: список, число или ничего. После неё цепочку не продолжить, поэтому терминальная операция стоит последней. toList() — терминальная операция.
Метод toList() появился в Java 16. В более старом коде вместо него пишут collect(Collectors.toList()): метод collect собирает элементы, а Collectors.toList() говорит ему, во что. Такую запись вы часто встретите в рабочих проектах. Список из toList() менять нельзя, как и список из List.of: его add бросит UnsupportedOperationException.
Итог одним числом
Иногда в конце ленты нужен не список, а одно число:
long n = words.stream().filter(w -> w.length() > 3).count();
System.out.println(n); // 2
int letters = words.stream().mapToInt(String::length).sum();
System.out.println(letters); // 15Разберём по строкам:
count()— терминальная операция: она считает, сколько элементов доехало до конца. Доехалиzebraиhorse, поэтому результат2. Тип результата —long.mapToInt(String::length)заменяет каждое слово числомint— его длиной. Получается особый стрим чисел,IntStream.sum()— терминальная операцияIntStream: она складывает3 + 5 + 2 + 5и даёт15.
Почему mapToInt, а не map? У обычного стрима нет метода sum: он не знает, что его элементы — числа. IntStream это знает, поэтому sum у него есть.
Частая ошибка
Результат count() положили в переменную int:
int n = words.stream().filter(w -> w.length() > 3).count(); // ✗error: incompatible types: possible lossy conversion from long to intПрограмма не компилируется: count() возвращает long, а в int такое число может не поместиться, и сам сужать тип компилятор не станет. Похожее сообщение вы уже видели в теме про операторы, только там был double. Объявите переменную как long n.
Главное
- Стрим — конвейер над элементами источника:
stream()выкладывает их на ленту, а сам источник не меняется. - Промежуточные операции (
filter,map) возвращают новый стрим, терминальные (toList,count,sum) — результат. filterпропускает элементы, для которыхPredicateвернулtrue, аmapзаменяет каждый элемент результатомFunction.count()возвращаетlong, аmapToInt(...).sum()складывает числа.