W3docs

Создание потоков Java

Создание потоков Java из коллекций, массивов, Stream.of, Stream.iterate, Stream.generate и источников ввода-вывода.

Вводная глава описала форму конвейера — источник → промежуточные операции → терминальная операция — и рассматривала источник как нечто уже готовое. Эта глава является каталогом источников. Каждый конвейер потока, который вы напишете, начинается с одного из них, и у каждого есть небольшой набор особенностей, которые определяют, будет ли следующий конвейер корректным, ленивым, конечным, упорядоченным или поддерживающим параллельное выполнение.

Список короче, чем кажется. Почти каждый поток, который вы когда-либо напишете, начинается с coll.stream(), Stream.of(...), Arrays.stream(arr) или IntStream.range. Остальная часть этой главы — «а вот несколько ситуаций, когда правильным выбором являются другие варианты».

Из Collectioncoll.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();   // 55

range(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 empty

Optional.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, Queuecoll.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()
Нужно стримить Mapmap.entrySet().stream()

Эта таблица охватывает всё из главы и, вероятно, 99% реального кода.

Практический пример: десять источников, одна программа

Программа ниже создаёт один поток из каждого из основных источников, запускает небольшую терминальную операцию, чтобы вывод был виден, и печатает как результат, так и вид источника.

java— editable, runs on the server

Что следует вынести из запуска:

  • Каждая строка в выводе поступила из разного источника, но все они используют одни и те же промежуточные и терминальные операции. Выбор источника определяет, с чего может начаться конвейер; он не меняет того, что идёт после.
  • Arrays.stream(int[]) создал IntStreamsum() доступен прямо на потоке, без упаковки, без 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 Operationsfilter, map, flatMap, distinct, sorted, peek, limit, skip — ленивые трансформации, которые изменяют форму потока без его выполнения. Затем терминальные операции, производящие значение.

Практика

Практика
Какой из этих способов является *самым дешёвым* для итерации целых чисел от `0` до `99` включительно в виде потока?
Какой из этих способов является *самым дешёвым* для итерации целых чисел от `0` до `99` включительно в виде потока?
Was this page helpful?