Большинство циклов в прикладном коде делают одно и то же: пройти по коллекции, что-то отфильтровать, что-то преобразовать и собрать результат. Stream API (в языке с Java 8, но в Java 21 заметно удобнее) описывает такие операции как конвейер, где каждый шаг говорит, что делается, а не как.
record Student(String name, int age, String city) {}
var students = java.util.List.of(
new Student("Анна", 17, "Москва"),
new Student("Борис", 19, "Казань"),
new Student("Вера", 16, "Москва"),
new Student("Глеб", 21, "Тула"));
// Цикл
var namesLoop = new java.util.ArrayList<String>();
for (var s : students) {
if (s.age() < 18) {
namesLoop.add(s.name().toUpperCase());
}
}
// Конвейер
var names = students.stream()
.filter(s -> s.age() < 18)
.map(s -> s.name().toUpperCase())
.toList();
System.out.println(names); // [АННА, ВЕРА]
Метод toList() появился в Java 16 и возвращает неизменяемый список. Он заменил громоздкое collect(Collectors.toList()) в большинстве случаев.
collection.stream(), Stream.of(...), IntStream.range(0, 10), Files.lines(path), text.lines().filter, map, sorted, distinct, limit, skip, flatMap, peek. Они ленивые: ничего не происходит, пока не вызвана терминальная операция.toList, collect, forEach, count, sum, max, findFirst, anyMatch, reduce. После них поток использовать нельзя.Ленивость означает, что filter и map применяются к элементам по одному, а не проходят по всей коллекции по очереди. Поэтому limit(3) после дорогого map действительно вычислит только три элемента.
Самая частая практическая задача — разложить элементы по корзинам. Для этого есть Collectors.groupingBy, и его результат — обычная Map:
import java.util.stream.Collectors;
var byCity = students.stream()
.collect(Collectors.groupingBy(Student::city));
// {Казань=[Student[...]], Тула=[...], Москва=[Student[Анна], Student[Вера]]}
var countByCity = students.stream()
.collect(Collectors.groupingBy(Student::city, Collectors.counting()));
// {Казань=1, Тула=1, Москва=2}
var averageAgeByCity = students.stream()
.collect(Collectors.groupingBy(Student::city, Collectors.averagingInt(Student::age)));
// {Казань=19.0, Тула=21.0, Москва=16.5}
Порядок ключей в комментариях — тот, что выдаёт JDK 21 на этих данных, но полагаться на него нельзя: groupingBy по умолчанию собирает результат в HashMap, а она не обещает никакого порядка. Второй аргумент groupingBy — коллектор, который применяется к каждой группе. Кроме counting и averagingInt пригодятся mapping, summingInt, maxBy и joining. Если нужен предсказуемый порядок ключей, передайте фабрику карты: groupingBy(Student::city, TreeMap::new, counting()).
import java.util.Comparator;
var oldestFirst = students.stream()
.sorted(Comparator.comparingInt(Student::age).reversed()
.thenComparing(Student::name))
.map(Student::name)
.toList(); // [Глеб, Борис, Анна, Вера]
var firstFromMoscow = students.stream()
.filter(s -> s.city().equals("Москва"))
.findFirst(); // Optional[Student[name=Анна, ...]]
boolean anyAdult = students.stream().anyMatch(s -> s.age() >= 18); // true
Результат findFirst и max — Optional, потому что поток может оказаться пустым. Как с ним работать, разобрано в следующем уроке.
Для чисел есть IntStream, LongStream и DoubleStream без упаковки в объекты. Они умеют sum, average, summaryStatistics:
var stats = students.stream().mapToInt(Student::age).summaryStatistics();
System.out.println(stats.getMin() + " … " + stats.getMax() + ", среднее " + stats.getAverage());
// 16 … 21, среднее 18.25
int sumOfSquares = java.util.stream.IntStream.rangeClosed(1, 10)
.map(n -> n * n)
.sum(); // 385
Когда у каждого элемента есть своя коллекция, map даст поток списков. flatMap разворачивает их в один поток элементов:
record Course(String title, java.util.List<String> tags) {}
var courses = java.util.List.of(
new Course("Java", java.util.List.of("код", "backend")),
new Course("SQL", java.util.List.of("данные", "backend")));
var uniqueTags = courses.stream()
.flatMap(c -> c.tags().stream())
.distinct()
.sorted()
.toList(); // [backend, данные, код]
Три ошибки новичков
forEach или map. Конвейер должен быть чистым; для накопления есть коллекторы и reduce.IllegalStateException. Создайте новый поток от источника.parallel() «для скорости». На маленьких коллекциях это медленнее, а с изменяемым состоянием ещё и неверно. Параллельные потоки — для больших массивов чистых вычислений.Если в теле цикла есть ранний выход с сложным условием, обработка исключений или несколько результатов сразу, обычный for часто читается проще. Конвейер хорош, когда шаги независимы и каждый умещается в одну строку.
students постройте Map<String, List<String>> «город → имена», используя groupingBy и mapping.groupingBy(w -> w, counting()), затем поток по entrySet(), сортировка по значению в обратном порядке и limit(5).