Создание потоков Java
Создание потоков Java из коллекций, массивов, Stream.of, Stream.iterate, Stream.generate и источников ввода-вывода.
Вводная глава описала форму конвейера — источник → промежуточные операции → терминальная операция — и рассматривала источник как нечто уже готовое. Эта глава является каталогом источников. Каждый конвейер потока, который вы напишете, начинается с одного из них, и у каждого есть небольшой набор особенностей, которые определяют, будет ли следующий конвейер корректным, ленивым, конечным, упорядоченным или поддерживающим параллельное выполнение.
Список короче, чем кажется. Почти каждый поток, который вы когда-либо напишете, начинается с coll.stream(), Stream.of(...), Arrays.stream(arr) или IntStream.range. Остальная часть этой главы — «а вот несколько ситуаций, когда правильным выбором являются другие варианты».
Из Collection — coll.stream()
Самый распространённый источник. У Collection<T> есть метод по умолчанию stream(), поэтому каждый List, Set, Queue и Deque предоставляет его бесплатно:
List<String> names = List.of("Alice", "Bob", "Carol");
long count = names.stream().filter(n -> n.length() > 3).count();Поток является последовательным, известного размера (JVM знает количество элементов заранее) и упорядоченным, если коллекция упорядочена. List создаёт упорядоченный поток; HashSet — неупорядоченный; TreeSet — упорядоченный, отсортированный по компаратору множества.
Существует также coll.parallelStream(), который планирует выполнение в общем ForkJoinPool. Тот же источник, другая политика выполнения — рассматривается в Java Parallel Streams.
Из явных элементов — Stream.of(...)
Используйте Stream.of, когда у вас есть короткий известный список элементов и вы не хотите создавать временный List:
Stream<String> s = Stream.of("a", "b", "c");
Stream<Integer> n = Stream.of(1, 2, 3, 4, 5);
Stream<Object> one = Stream.of("just one");Это varargs-метод, поэтому он принимает любое количество аргументов (ноль допустим и даёт пустой поток). При одном аргументе T[] компилятор выбирает Stream.of(T...), а не Stream.of(T) — удобно, когда у вас уже есть массив:
String[] arr = {"x", "y", "z"};
Stream<String> fromArr = Stream.of(arr); // same as Arrays.stream(arr)Из массива — Arrays.stream(...)
У Arrays.stream есть перегрузки для T[], int[], long[], double[], а также варианты с диапазоном:
int[] xs = {3, 1, 4, 1, 5, 9, 2, 6};
IntStream ix = Arrays.stream(xs); // primitive specialisation
IntStream tail = Arrays.stream(xs, 2, xs.length); // half-open [2, len)
String[] words = {"alpha", "beta", "gamma"};
Stream<String> ws = Arrays.stream(words);Примитивные перегрузки возвращают IntStream, LongStream, DoubleStream — а не Stream<Integer>. Это важно: примитивные потоки избегают упаковки, имеют sum, average, min, max напрямую (без коллектора) и хорошо работают с mapToInt/mapToObj для перехода между мирами.
Примитивные диапазоны — IntStream.range / rangeClosed
Самый быстрый способ итерировать по индексу без цикла for:
// 0, 1, 2, ..., 9
IntStream.range(0, 10).forEach(i -> System.out.println(i));
// 1..10 inclusive
int sum = IntStream.rangeClosed(1, 10).sum(); // 55range(a, b) — полуоткрытый [a, b). rangeClosed(a, b) — [a, b]. Оба ограничены, упорядочены, известного размера и быстрее, чем Stream.iterate(0, i -> i + 1).limit(n), потому что JVM знает количество элементов заранее. Используйте их всякий раз, когда тело цикла — «сделать что-то по индексу i».
Чтобы связать индекс с элементами List, пишут:
List<String> names = List.of("Alice", "Bob", "Carol");
IntStream.range(0, names.size())
.mapToObj(i -> i + ": " + names.get(i))
.forEach(System.out::println);Генерируемые бесконечные потоки — Stream.iterate и Stream.generate
Два способа создать неограниченный поток. Они выглядят похоже, но это не одно и то же.
Stream.iterate(seed, f) — начинает с seed, затем f(seed), затем f(f(seed)), …. Упорядоченный, детерминированный, последовательный. Почти всегда за ним следует операция с коротким замыканием:
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println); // 1, 2, 4, 8, ..., 512Существует также трёхаргументная перегрузка Stream.iterate(seed, hasNext, next) (Java 9+), которая встраивает условие остановки прямо в источник — limit не нужен:
Stream.iterate(1, n -> n < 1000, n -> n * 2).forEach(System.out::println);Stream.generate(supplier) — многократно вызывает Supplier<T>. Неупорядоченный, связи между элементами нет:
Stream.generate(Math::random).limit(5).forEach(System.out::println);
Stream.generate(() -> "ping").limit(3).forEach(System.out::println);Используйте iterate для последовательностей, где каждый член зависит от предыдущего (n -> n + 1, n -> n * 2, пара Фибоначчи arr -> {arr[1], arr[0] + arr[1]}). Используйте generate для независимых значений из стороннего источника — случайных чисел, фиксированных констант, UUID.
В любом случае всегда завершайте их операцией с коротким замыканием: limit(n), трёхаргументный iterate, или терминальной операцией вроде findFirst / anyMatch. Обычный toList() на бесконечном потоке зависает JVM.
Из ввода-вывода — Files.lines, BufferedReader.lines
Files.lines(path) открывает файл и возвращает Stream<String> его строк. Ленивый: строки читаются по мере того, как конвейер их запрашивает, а не заранее:
try (Stream<String> lines = Files.lines(Path.of("words.txt"))) {
long longWords = lines.filter(w -> w.length() > 8).count();
}try-with-resources обязателен. Поток удерживает открытый файловый дескриптор, и единственный способ освободить его — вызвать close() — что try-with-resources делает за вас. Без этого дескриптор утечёт до тех пор, пока поток не будет собран сборщиком мусора, что при нагрузке может не произойти никогда.
Та же форма для Reader через BufferedReader.lines(). Оба являются канонически правильным способом обхода текстового файла без загрузки его в память.
String.chars() и String.codePoints()
String — это последовательность кодовых единиц UTF-16; API предоставляет оба представления:
"hello".chars() // IntStream of UTF-16 code units
.filter(Character::isUpperCase)
.count();
"héllo".codePoints() // IntStream of Unicode code points
.mapToObj(Character::toString)
.forEach(System.out::println);Оба возвращают IntStream. chars() подходит для ASCII; для всего, что может содержать суррогатные пары (большинство эмодзи, многие скрипты), codePoints() — безопасный выбор.
Пустые потоки и потоки с одним элементом
Для случаев по умолчанию и ветвей flatMap:
Stream<String> none = Stream.empty(); // 0 elements
Stream<String> one = Stream.of("x"); // exactly 1
Stream<String> opt = Optional.of("x").stream(); // 1 if present, else emptyOptional.stream() (Java 9+) — мост между Optional<T> и Stream<T> — удобен, когда вы выполняете flatMap потока Optional-ов в поток присутствующих значений без какой-либо работы с null.
Stream.Builder — добавление элементов по одному
Когда нельзя выразить источник в виде литерала, массива или генератора — обычно потому, что элементы поступают из разрозненных ветвей императивного кода, — существует строитель:
Stream.Builder<String> b = Stream.builder();
b.add("first");
if (someCondition) b.add("second");
b.accept("third");
Stream<String> s = b.build();После build() строитель запечатан; дальнейший add бросает исключение. Это редкий, но честный инструмент. Большинство кода, тянущегося к нему, лучше написать с ArrayList<String> с последующим list.stream(), но строитель избегает этой промежуточной структуры, когда данные формируются поэтапно.
Потоки Map — их не существует
У Map<K, V> нет метода stream(). Вместо этого вы стримите его представления:
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25);
ages.entrySet().stream().filter(e -> e.getValue() >= 18).map(Map.Entry::getKey).toList();
ages.keySet().stream().sorted().toList();
ages.values().stream().mapToInt(Integer::intValue).sum();entrySet().stream() — это то, что чаще всего нужно: обе половины каждой записи доступны, и Map.Entry::getKey / ::getValue работают как ссылки на методы.
Выбор правильного источника
| Ситуация | Использовать |
|---|---|
Уже есть List, Set, Queue | coll.stream() |
| Есть несколько фиксированных элементов | Stream.of(a, b, c) |
Есть T[] | Arrays.stream(arr) |
Есть int[], long[], double[] | Arrays.stream(arr) → примитивный поток |
| Нужно итерировать по индексу | IntStream.range(0, n) |
| Каждый элемент зависит от предыдущего | Stream.iterate |
| Нужны независимые выборки | Stream.generate |
| Нужны строки текстового файла | Files.lines(path) внутри try-with-resources |
Нужны символы String | "...".chars() или .codePoints() |
| Нужен запасной пустой поток | Stream.empty() |
| Элементы строятся поэтапно | Stream.builder() |
Нужно стримить Map | map.entrySet().stream() |
Эта таблица охватывает всё из главы и, вероятно, 99% реального кода.
Практический пример: десять источников, одна программа
Программа ниже создаёт один поток из каждого из основных источников, запускает небольшую терминальную операцию, чтобы вывод был виден, и печатает как результат, так и вид источника.
Что следует вынести из запуска:
- Каждая строка в выводе поступила из разного источника, но все они используют одни и те же промежуточные и терминальные операции. Выбор источника определяет, с чего может начаться конвейер; он не меняет того, что идёт после.
Arrays.stream(int[])создалIntStream—sum()доступен прямо на потоке, без упаковки, безCollectors.summingInt. Примитивные специализации важны в числовых конвейерах.- Два вызова
Stream.iterateдемонстрируют разницу междуiterate(seed, f)+limit(n)(вы выбираете количество) и трёхаргументнымiterate(seed, hasNext, next)(источник выбирает количество). Оба ограничены;iterateбез границы и без терминальной операции с коротким замыканием — классическая ошибка зависания JVM. Stream.empty()иOptional.of(...).stream()— это то, как пустые потоки и потоки с одним элементом входят в конвейер — обычно внутри ветвиflatMap, где некоторые входные данные производят ноль или один элемент downstream.Stream.builder()— это аварийный выход для (редкого) случая, когда источник строится императивно по ветвям. Большинство реального кода сначала тянется кcoll.stream().
Что дальше
Теперь вы можете создать любой нужный поток из любого имеющегося источника. Следующие две главы охватывают операции, выполняемые между источником и результатом. Сначала Java Stream Intermediate Operations — filter, map, flatMap, distinct, sorted, peek, limit, skip — ленивые трансформации, которые изменяют форму потока без его выполнения. Затем терминальные операции, производящие значение.