Методы Stream API - Вопросы

Всего: 7 вопросов

1. 

Как отличить терминальную операцию Stream API от промежуточной?

По возвращаемому типу. Если метод возвращает Stream, IntStream, LongStream или DoubleStream, он промежуточный: он только достраивает конвейер и не выполняет переданную лямбду. Если метод возвращает значение, коллекцию, массив, Optional, Iterator, Spliterator или void, он терминальный: запускает весь конвейер, отдаёт результат и закрывает поток.

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

2. 

Какие формы есть у метода reduce() и когда он возвращает Optional?

reduce() — терминальная операция, которая последовательно применяет бинарную операцию к элементам и сворачивает поток в одно значение. Форма с начальным значением (identity) всегда даёт результат, поэтому возвращает обычный тип T. Форма без начального значения на пустом потоке возвращать нечего, поэтому её результат обёрнут в Optional<T>. Третья форма принимает ещё и функцию объединения частичных результатов и нужна для параллельных потоков.

int sum = Stream.of(1, 2, 3, 4, 5)
        .reduce(0, Integer::sum);              // 15, Optional не нужен

Optional<Integer> max = Stream.of(1, 2, 3, 4, 5)
        .reduce(Integer::max);                 // Optional[5]

String sentence = Stream.of("Stream", "API", "reduce")
        .reduce("", (a, b) -> a.isEmpty() ? b : a + " " + b); // Stream API reduce

Для типовых задач специализированные методы удобнее: IntStream.sum() вместо reduce(0, Integer::sum) и Collectors.joining(" ") вместо ручной склейки строк. reduce() нужен там, где готового коллектора нет.

3. 

Как получить из потока массив и чем отличаются варианты toArray()?

toArray() — терминальная операция. Без аргументов она возвращает Object[], потому что из-за стирания типов рантайм не знает фактический тип элементов. Чтобы получить типизированный массив, передают ссылку на конструктор массива, например String[]::new. У примитивных потоков toArray() сразу отдаёт int[], long[] или double[].

Object[] objects = Stream.of("a", "b", "c").toArray();

String[] letters = Stream.of("a", "b", "c").toArray(String[]::new);
System.out.println(Arrays.toString(letters)); // [a, b, c]

int[] numbers = IntStream.rangeClosed(1, 5).toArray();
System.out.println(Arrays.toString(numbers)); // [1, 2, 3, 4, 5]
4. 

Зачем у потока есть методы iterator() и spliterator() и чем они отличаются?

Оба метода терминальные: они закрывают поток и возвращают итератор, через который элементы читаются вручную. Это нужно, когда результат надо передать в старый API, ожидающий Iterator, или когда обход должен прерываться по сложному условию, которое неудобно выразить через filter() и takeWhile().

Spliterator (splittable iterator) отличается от обычного итератора тем, что умеет делиться пополам методом trySplit() и сообщает свои характеристики (размер, отсортированность, уникальность). Именно на нём построена работа параллельных потоков.

Iterator<String> it = Stream.of("sun", "sea", "sand").iterator();
while (it.hasNext()) {
    System.out.println(it.next()); // sun, sea, sand
}

Spliterator<String> sp = Stream.of("sun", "sea", "sand").spliterator();
sp.tryAdvance(s -> System.out.println("first: " + s)); // first: sun
sp.forEachRemaining(System.out::println);              // sea, sand
5. 

Чем forEach() отличается от forEachOrdered()?

В последовательном потоке они работают одинаково. Разница проявляется в параллельном потоке: forEach() не даёт никаких гарантий порядка и обрабатывает элементы так, как их отдают рабочие потоки, а forEachOrdered() обязан пройти элементы строго в порядке источника.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6, 7, 8);

nums.parallelStream().forEach(System.out::print);
// например 56781234 - порядок произвольный и меняется от запуска к запуску

nums.parallelStream().forEachOrdered(System.out::print);
// всегда 12345678

forEachOrdered() не бесплатен: он заставляет параллельный поток синхронизировать выдачу результатов и часто съедает весь выигрыш от параллелизма. Если порядок важен, обычно проще собрать результат через collect() — коллекторы сохраняют порядок источника сами.

6. 

Чем findFirst() отличается от findAny()?

findFirst() всегда возвращает первый элемент в порядке источника. findAny() возвращает любой доступный элемент и потому дешевле в параллельном потоке: ему не нужно дожидаться, пока отработает часть, отвечающая за начало данных. В последовательном потоке результаты обычно совпадают, но контракт этого не гарантирует — полагаться на совпадение нельзя.

Оба метода терминальные и возвращают Optional<T>, потому что поток может оказаться пустым и возвращать будет нечего.

7. 

Чем stateless-операции отличаются от stateful и почему это важно?

Stateless (без сохранения состояния) — операция обрабатывает каждый элемент независимо и ничего не помнит о предыдущих: filter(), map(), flatMap(), peek(). Такие операции идеально распараллеливаются и не требуют дополнительной памяти.

Stateful (с сохранением состояния) — операция должна увидеть другие элементы, иногда весь поток целиком, прежде чем отдать результат: sorted(), distinct(), limit(), skip(), takeWhile(), dropWhile(). sorted() и distinct() вынуждены буферизовать данные, а limit() и skip() держат счётчик обработанных элементов.

Практический вывод: stateful-операции стоят дороже, их выгодно ставить после фильтрации, а на бесконечном потоке sorted() и distinct() просто зависнут — они ждут конца данных, которого не будет.

Страница 1 из 1