Что такое Stream API в Java - Вопросы
Всего: 5 вопросов
1. Что такое Stream API в Java и чем Stream отличается от коллекции?
Что такое Stream API в Java и чем Stream отличается от коллекции?
Stream API — это набор классов и интерфейсов пакета java.util.stream, появившийся в Java 8. Он позволяет обрабатывать последовательности элементов в декларативном (функциональном) стиле: вы описываете, что нужно сделать с данными, а не как обходить их циклом.
Коллекция — это структура данных: она хранит элементы в памяти, знает свой размер, даёт доступ по индексу и позволяет добавлять и удалять элементы. Stream ничего не хранит: он получает элементы из источника (коллекции, массива, файла, генератора), пропускает их через конвейер операций и закрывается.
Отсюда ключевые отличия: поток не изменяет источник (filter(), map(), sorted() возвращают новый поток, а исходная коллекция остаётся нетронутой), поток одноразовый (повторное обращение после терминальной операции бросает IllegalStateException), у потока нет доступа по индексу и его операции ленивые. Коллекцию можно обходить сколько угодно раз, поток — только один раз.
2. Какими способами можно создать Stream в Java?
Какими способами можно создать Stream в Java?
Основных способов семь:
- Из коллекции —
words.stream(), метод есть у любойCollection. - Из перечисленных значений —
Stream.of("hello", "hola", "ciao"). - Из массива объектов —
Arrays.stream(words)илиStream.of(words). - Из массива примитивов —
Arrays.stream(nums)илиIntStream.of(nums); получитсяIntStream, а неStream<Integer>. - Генератором и итератором —
Stream.generate(Math::random).limit(5),Stream.iterate(1, n -> n * 2).limit(5); такие потоки бесконечны и требуют ограничителя (с Java 9 есть трёхаргументныйStream.iterate(1, n -> n < 100, n -> n * 2)с условием остановки). - Через билдер —
Stream.<String>builder().add("h").add("i").build(). - Диапазоном чисел —
IntStream.range(1, 4)(1, 2, 3) иIntStream.rangeClosed(1, 4)(1, 2, 3, 4).
Есть и другие источники: Files.lines(path) для строк файла, String.chars() для символов, Random.ints(), Pattern.splitAsStream() и Stream.empty() для пустого потока.
3. Какие типичные ошибки чаще всего допускают при работе с Stream API в Java?
Какие типичные ошибки чаще всего допускают при работе с Stream API в Java?
Несколько ошибок встречаются регулярно, даже у опытных разработчиков:
- Повторное использование потока. После терминальной операции поток закрыт; второе обращение бросает
IllegalStateException. Если нужен второй проход — создавайте поток заново или хранитеSupplier<Stream<T>>. - Забытая терминальная операция. Цепочка из одних промежуточных операций (
filter,mapи т.д.) не делает ничего и не выдаёт ошибку — код молча не выполняется, потому что промежуточные операции ленивые. Stream.of()с массивом примитивов.Stream.of(new int[]{1, 2, 3})даётStream<int[]>из одного элемента, а не поток чисел. НужноArrays.stream(nums)илиIntStream.of(nums).- Бесконечный поток без ограничителя.
Stream.generate(Math::random).forEach(...)будет крутиться вечно. Потоки отgenerate()и двухаргументногоiterate()обязательно ограничивают черезlimit()илиtakeWhile(). - Изменение источника во время обхода. Добавление или удаление элементов коллекции внутри лямбды приводит к
ConcurrentModificationException:List<String> list = new ArrayList<>(List.of("a", "b", "c")); list.stream().forEach(s -> { if (s.equals("b")) list.remove(s); // ConcurrentModificationException }); - Побочные эффекты вместо сбора результата. Вместо
forEach(result::add)используйтеcollect()илиtoList()— это безопаснее, особенно в параллельных потоках, где порядок и синхронизация побочных эффектов не гарантированы.
4. Чем IntStream отличается от Stream<Integer> и когда использовать примитивные потоки?
Чем IntStream отличается от Stream<Integer> и когда использовать примитивные потоки?
Stream<Integer> — это поток объектов: каждое число упаковывается в Integer (автобоксинг), что создаёт нагрузку на память и сборщик мусора. IntStream (а также LongStream и DoubleStream) работает с примитивами напрямую, без упаковки.
Второе отличие — набор методов. У примитивных потоков есть числовые операции, которых нет у Stream<T>: sum(), average(), max(), min(), summaryStatistics().
// Stream<Integer>: боксинг на каждом элементе, метода sum() нет
int total1 = List.of(1, 2, 3).stream().mapToInt(Integer::intValue).sum();
// IntStream: примитивы, sum() есть
int total2 = IntStream.of(1, 2, 3).sum(); // 6
double avg = IntStream.of(1, 2, 3).average().orElse(0); // 2.0
Переходы между ними: mapToInt(), mapToLong(), mapToDouble() — из объектного потока в примитивный; boxed() или mapToObj() — обратно. Используйте примитивные потоки для числовых вычислений и агрегаций, объектные — там, где нужны сами объекты.
5. Всегда ли parallelStream() быстрее обычного потока?
Всегда ли parallelStream() быстрее обычного потока?
Нет. Параллельный поток разбивает данные на части, обрабатывает их в общем пуле ForkJoinPool.commonPool() и объединяет результаты. На небольших коллекциях эти накладные расходы на разбиение и слияние больше самой полезной работы, поэтому parallelStream() оказывается медленнее последовательного потока.
Параллелизм оправдан, когда совпадают несколько условий: большой объём данных, независимые операции без побочных эффектов и общего изменяемого состояния, хорошо разбиваемый источник (ArrayList, массив, IntStream.range), а не LinkedList или поток из файла.
Отдельный риск: все параллельные потоки по умолчанию делят один общий ForkJoinPool, поэтому долгая или блокирующая задача в одном месте приложения тормозит все остальные. Решение о параллелизме принимается по результатам замеров, а не по умолчанию.