Что такое поток данных
Поток данных (stream) — это цепочка операций над последовательностью элементов: «возьми список, оставь нужное, преобразуй, собери результат». Поток не хранит данные и не меняет исходную коллекцию — он лишь описывает, что с элементами сделать. Не путайте его с потоками выполнения (threads) из главы о многопоточности: это разные вещи с похожим названием. Впрочем, поток данных можно обработать и в нескольких потоках выполнения сразу — об этом урок параллельные потоки.
Первый поток
Вместо цикла с условием можно описать задачу целиком: из списка чисел оставить чётные и вывести их.
void main() {
List<Integer> numbers = List.of(5, 2, 8, 3, 4);
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> IO.println(n));
}Результат:
2
8
4Метод stream() есть у любой коллекции. Дальше идут операции, каждая получает лямбду — короткую функцию, о которой шла речь в главе об обобщениях и лямбдах.
Три части потока
Любой поток состоит из трёх частей:
- Источник — откуда берутся элементы: коллекция, массив, набор значений.
- Промежуточные операции —
filter,map,sortedи другие. Каждая возвращает новый поток, поэтому их можно соединять в цепочку. - Завершающая операция —
forEach,count,toListи другие. Она запускает вычисление и выдаёт результат.
void main() {
List<String> names = List.of("Анна", "Олег", "Ира", "Андрей");
List<String> result = names.stream() // источник
.filter(n -> n.startsWith("А")) // промежуточная
.map(n -> n.toUpperCase()) // промежуточная
.toList(); // завершающая
IO.println(result);
}Результат:
[АННА, АНДРЕЙ]Откуда берутся потоки
Поток можно получить не только из списка:
void main() {
Stream<String> words = Stream.of("кот", "дом", "сад");
IO.println(words.count());
int[] prices = {120, 80, 45};
IO.println(Arrays.stream(prices).sum());
IntStream.range(1, 4).forEach(i -> IO.println("Шаг " + i));
}Результат:
3
245
Шаг 1
Шаг 2
Шаг 3Stream.of создаёт поток из перечисленных значений, Arrays.stream — из массива. IntStream.range(1, 4) выдаёт числа от 1 до 3: правая граница не входит. IntStream — особый поток для чисел int с удобными методами вроде sum().
Ленивые вычисления
Промежуточные операции ленивые: пока нет завершающей операции, они не выполняются. Чтобы это увидеть, добавим вывод внутрь filter:
void main() {
Stream<Integer> stream = Stream.of(1, 2, 3)
.filter(n -> {
IO.println("Проверяю " + n);
return n > 1;
});
IO.println("Поток создан");
IO.println(stream.toList());
}Результат:
Поток создан
Проверяю 1
Проверяю 2
Проверяю 3
[2, 3]Строки «Проверяю» появились только после toList(). Благодаря лени поток не делает лишней работы: например, findFirst() остановится на первом подходящем элементе.
Поток против цикла
Поток не заменяет циклы полностью. Он хорош, когда задача звучит как «отбери, преобразуй, посчитай». Если нужно досрочно выйти по сложному условию или менять несколько переменных сразу, обычный for часто понятнее. Выбирайте то, что легче прочитать.
Частая ошибка
Поток можно пройти только один раз. После завершающей операции он «израсходован», и повторный вызов выбросит исключение:
void main() {
Stream<String> stream = Stream.of("a", "b");
IO.println(stream.count());
IO.println(stream.count());
}Результат:
2
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at Main.main(Main.java:4)Если данные нужны повторно, храните их в коллекции и создавайте новый поток вызовом stream() каждый раз.