Что такое Stream API в Java - Вопросы

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

1. 

Что такое Stream API в Java и чем Stream отличается от коллекции?

Stream API — это набор классов и интерфейсов пакета java.util.stream, появившийся в Java 8. Он позволяет обрабатывать последовательности элементов в декларативном (функциональном) стиле: вы описываете, что нужно сделать с данными, а не как обходить их циклом.

Коллекция — это структура данных: она хранит элементы в памяти, знает свой размер, даёт доступ по индексу и позволяет добавлять и удалять элементы. Stream ничего не хранит: он получает элементы из источника (коллекции, массива, файла, генератора), пропускает их через конвейер операций и закрывается.

Отсюда ключевые отличия: поток не изменяет источник (filter(), map(), sorted() возвращают новый поток, а исходная коллекция остаётся нетронутой), поток одноразовый (повторное обращение после терминальной операции бросает IllegalStateException), у потока нет доступа по индексу и его операции ленивые. Коллекцию можно обходить сколько угодно раз, поток — только один раз.

2. 

Какими способами можно создать Stream в Java?

Основных способов семь:

  1. Из коллекцииwords.stream(), метод есть у любой Collection.
  2. Из перечисленных значенийStream.of("hello", "hola", "ciao").
  3. Из массива объектовArrays.stream(words) или Stream.of(words).
  4. Из массива примитивовArrays.stream(nums) или IntStream.of(nums); получится IntStream, а не Stream<Integer>.
  5. Генератором и итератором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) с условием остановки).
  6. Через билдерStream.<String>builder().add("h").add("i").build().
  7. Диапазоном чисел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?

Несколько ошибок встречаются регулярно, даже у опытных разработчиков:

  1. Повторное использование потока. После терминальной операции поток закрыт; второе обращение бросает IllegalStateException. Если нужен второй проход — создавайте поток заново или храните Supplier<Stream<T>>.
  2. Забытая терминальная операция. Цепочка из одних промежуточных операций (filter, map и т.д.) не делает ничего и не выдаёт ошибку — код молча не выполняется, потому что промежуточные операции ленивые.
  3. Stream.of() с массивом примитивов. Stream.of(new int[]{1, 2, 3}) даёт Stream<int[]> из одного элемента, а не поток чисел. Нужно Arrays.stream(nums) или IntStream.of(nums).
  4. Бесконечный поток без ограничителя. Stream.generate(Math::random).forEach(...) будет крутиться вечно. Потоки от generate() и двухаргументного iterate() обязательно ограничивают через limit() или takeWhile().
  5. Изменение источника во время обхода. Добавление или удаление элементов коллекции внутри лямбды приводит к ConcurrentModificationException:
    List<String> list = new ArrayList<>(List.of("a", "b", "c"));
    list.stream().forEach(s -> {
        if (s.equals("b")) list.remove(s); // ConcurrentModificationException
    });
  6. Побочные эффекты вместо сбора результата. Вместо forEach(result::add) используйте collect() или toList() — это безопаснее, особенно в параллельных потоках, где порядок и синхронизация побочных эффектов не гарантированы.
4. 

Чем 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() быстрее обычного потока?

Нет. Параллельный поток разбивает данные на части, обрабатывает их в общем пуле ForkJoinPool.commonPool() и объединяет результаты. На небольших коллекциях эти накладные расходы на разбиение и слияние больше самой полезной работы, поэтому parallelStream() оказывается медленнее последовательного потока.

Параллелизм оправдан, когда совпадают несколько условий: большой объём данных, независимые операции без побочных эффектов и общего изменяемого состояния, хорошо разбиваемый источник (ArrayList, массив, IntStream.range), а не LinkedList или поток из файла.

Отдельный риск: все параллельные потоки по умолчанию делят один общий ForkJoinPool, поэтому долгая или блокирующая задача в одном месте приложения тормозит все остальные. Решение о параллелизме принимается по результатам замеров, а не по умолчанию.

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