Методы Stream API - Вопросы
Всего: 7 вопросов
1. Как отличить терминальную операцию Stream API от промежуточной?
Как отличить терминальную операцию Stream API от промежуточной?
По возвращаемому типу. Если метод возвращает Stream, IntStream, LongStream или DoubleStream, он промежуточный: он только достраивает конвейер и не выполняет переданную лямбду. Если метод возвращает значение, коллекцию, массив, Optional, Iterator, Spliterator или void, он терминальный: запускает весь конвейер, отдаёт результат и закрывает поток.
Любая цепочка устроена одинаково: источник, затем ноль или больше промежуточных операций, затем ровно одна терминальная. Пока терминальной операции нет, код компилируется и работает, но не делает ничего — все промежуточные операции ленивые.
2. Какие формы есть у метода reduce() и когда он возвращает Optional?
Какие формы есть у метода 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()?
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() и чем они отличаются?
Зачем у потока есть методы 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()?
В последовательном потоке они работают одинаково. Разница проявляется в параллельном потоке: 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()?
findFirst() всегда возвращает первый элемент в порядке источника. findAny() возвращает любой доступный элемент и потому дешевле в параллельном потоке: ему не нужно дожидаться, пока отработает часть, отвечающая за начало данных. В последовательном потоке результаты обычно совпадают, но контракт этого не гарантирует — полагаться на совпадение нельзя.
Оба метода терминальные и возвращают Optional<T>, потому что поток может оказаться пустым и возвращать будет нечего.
7. Чем stateless-операции отличаются от stateful и почему это важно?
Чем stateless-операции отличаются от stateful и почему это важно?
Stateless (без сохранения состояния) — операция обрабатывает каждый элемент независимо и ничего не помнит о предыдущих: filter(), map(), flatMap(), peek(). Такие операции идеально распараллеливаются и не требуют дополнительной памяти.
Stateful (с сохранением состояния) — операция должна увидеть другие элементы, иногда весь поток целиком, прежде чем отдать результат: sorted(), distinct(), limit(), skip(), takeWhile(), dropWhile(). sorted() и distinct() вынуждены буферизовать данные, а limit() и skip() держат счётчик обработанных элементов.
Практический вывод: stateful-операции стоят дороже, их выгодно ставить после фильтрации, а на бесконечном потоке sorted() и distinct() просто зависнут — они ждут конца данных, которого не будет.