JavaRush /Курсы /JAVA 25 SELF /Примитивные стримы и стоимость упаковки (boxing)

Примитивные стримы и стоимость упаковки (boxing)

JAVA 25 SELF
33 уровень, 0 лекция
Открыта

1. Проблема: почему обычные Stream не всегда эффективны

Когда вы работаете с коллекциями чисел в Java и используете обычные стримы (Stream<Integer>, Stream<Double>), под капотом происходит «упаковка» (boxing) и «распаковка» (unboxing) примитивных значений в объекты-обёртки (Integer, Double и т.д.). Это удобно, но не всегда эффективно:

  • Boxing — превращение примитива (int) в объект (Integer).
  • Unboxing — обратная операция: из объекта в примитив.

Проблема:
Boxing/unboxing — это лишние операции и трата памяти. В горячих (часто вызываемых) местах программы это может привести к заметному падению производительности, особенно если вы обрабатываете большие массивы чисел.

Пример:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);

Здесь каждое число — это объект Integer, а не примитив int.

2. Примитивные стримы: IntStream, LongStream, DoubleStream

Чтобы избежать лишнего boxing/unboxing, в Java есть примитивные стримы:

  • IntStream — для int
  • LongStream — для long
  • DoubleStream — для double

Они работают только с примитивами и не создают лишних объектов-обёрток.

Как создать примитивный стрим?

Из массива:

int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);

С помощью range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 включительно

Генерация случайных чисел:

new Random().ints(5, 0, 100)    // 5 случайных int от 0 до 99

Пример: суммирование элементов массива

int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15

3. Преобразование между Stream<T> и примитивными стримами

Иногда у вас есть обычный стрим, а иногда — примитивный. Для преобразования используются специальные методы:

  • mapToInt, mapToLong, mapToDouble — преобразуют обычный стрим в примитивный.
  • boxed() — превращает примитивный стрим обратно в стрим объектов.

Пример:

List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3

Обратно:

IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();

Внимание:
boxed() — это обратная операция, она снова создаёт объекты-обёртки (Integer, Double и т.д.), и если вам нужна максимальная производительность — избегайте её в «горячих» местах.

4. Суммирование и сводки: sum, average, min, max, summaryStatistics

Примитивные стримы имеют удобные методы для агрегации:

  • sum() — сумма всех элементов
  • average() — среднее (возвращает OptionalDouble)
  • min(), max() — минимум и максимум (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — возвращает объект с полной статистикой (сумма, среднее, минимум, максимум, количество)

Пример:

int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum());      // 15
System.out.println(stats.getAverage());  // 3.0
System.out.println(stats.getMin());      // 1
System.out.println(stats.getMax());      // 5
System.out.println(stats.getCount());    // 5

Сравнение с Collectors:
Для обычных стримов можно использовать Collectors.summarizingInt, но это чаще менее эффективно, чем методы примитивных стримов:

List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));

5. Избегаем автобоксинга: где это важно и как измерить

Где это критично?

  • В циклах и стримах, которые обрабатывают большие объёмы чисел (например, обработка массивов, статистика, математика, парсинг данных).
  • В «горячих» местах — код, который вызывается часто и влияет на производительность.

Почему это важно?

  • Каждый раз, когда происходит boxing, создаётся новый объект-обёртка (Integer, Double и т.д.).
  • Это увеличивает нагрузку на сборщик мусора (GC).
  • В микробенчмарках разница может быть в разы!

Как измерить?
Для точного сравнения используют фреймворк JMH (Java Microbenchmark Harness). Он позволяет честно сравнить производительность кода с boxing и без.

Пример (псевдокод):

@Benchmark
public int sumIntStream() {
    return IntStream.range(0, 1_000_000).sum();
}

@Benchmark
public int sumStreamInteger() {
    return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}

Второй вариант будет работать медленнее из‑за постоянного создания объектов Integer.

Вывод:
Если вам важна производительность — используйте примитивные стримы!

6. Когда примитивные стримы реально помогают, а когда не стоит усложнять

Используйте примитивные стримы, если:

  • Вы работаете с большими массивами/коллекциями чисел.
  • Вам нужно быстро посчитать сумму, среднее, минимум, максимум.
  • Вы пишете код, где важна каждая миллисекунда (например, обработка данных в реальном времени).

Можно не заморачиваться, если:

  • Коллекция небольшая (десятки-единицы элементов).
  • Код становится слишком сложным из-за преобразований между типами.
  • Производительность не критична (например, обработка пользовательского ввода).

Пример:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Можно, но и обычный reduce сойдёт

Совет: не превращайте код в «джунгли» ради микроскопической оптимизации. Используйте примитивные стримы там, где это действительно оправдано.

7. OptionalInt, OptionalDouble: безопасное извлечение результатов

Методы min(), max(), average() у примитивных стримов возвращают не просто число, а «обёртку» — OptionalInt, OptionalDouble и т.д. Это нужно, чтобы безопасно обрабатывать пустые стримы (например, если массив пустой).

Пример:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Минимум: " + min.getAsInt());
} else {
    System.out.println("Массив пустой!");
}

Сравнение с обычным Optional:

  • OptionalInt — для int
  • OptionalDouble — для double
  • OptionalLong — для long

Почему не просто возвращать 0?
Потому что 0 может быть валидным значением, а пустой стрим — это отдельная ситуация.

Пример с average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // если пусто — вернёт NaN

8. Практика: примеры использования примитивных стримов

Пример 1: Сумма квадратов чисел от 1 до 1000

int sum = IntStream.rangeClosed(1, 1000)
                   .map(x -> x * x)
                   .sum();
System.out.println(sum);

Пример 2: Фильтрация и подсчёт чётных чисел

int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
                   .filter(x -> x % 2 == 0)
                   .count();
System.out.println("Чётных чисел: " + count);

Пример 3: Сравнение с обычным Stream<Integer>

List<Integer> list = IntStream.range(0, 1_000_000)
                              .boxed()
                              .collect(Collectors.toList());

long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");

t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");

На больших объёмах разница будет заметна.

9. Типичные ошибки при работе с примитивными стримами

Ошибка №1: Забыли про boxing при преобразовании типов.
Если вы используете boxed(), помните, что это снова создаёт объекты-обёртки. Не используйте без необходимости.

Ошибка №2: Использование примитивных стримов для объектов.
IntStream работает только с int, а не с объектами. Если вам нужно работать с объектами — используйте обычный Stream<T>.

Ошибка №3: Игнорирование OptionalInt/OptionalDouble.
Если вы вызываете min(), max(), average() — всегда проверяйте, что результат есть (isPresent()), иначе получите исключение.

Ошибка №4: Слишком сложные преобразования между Stream<T> и IntStream.
Если код становится нечитаемым из‑за постоянных mapToInt() → boxed() → mapToDouble() — возможно, стоит упростить логику.

Ошибка №5: Ожидание «магического» ускорения на маленьких коллекциях.
Для маленьких списков разница между Stream и IntStream минимальна. Не стоит усложнять код ради микроскопической экономии.

1
Задача
JAVA 25 SELF, 33 уровень, 0 лекция
Недоступна
Подсчёт ежедневной выручки магазина 💰
Подсчёт ежедневной выручки магазина 💰
1
Задача
JAVA 25 SELF, 33 уровень, 0 лекция
Недоступна
Анализ длины заклинаний для магической академии ✨
Анализ длины заклинаний для магической академии ✨
Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Victor Уровень 50
26 августа 2026
26.08.2026
Олег Уровень 40
10 августа 2026
Чем дальше, тем больше крепнет дружба с ИИ
Anna Klushkina Уровень 2
7 августа 2026
33 ⛏️
SB Уровень 115
15 июля 2026
33! Ура, товарищи! ))
Максим Li Уровень 51
1 апреля 2026
33
Andrey Уровень 6
27 сентября 2025
уровень 33 - половина пути)
Big198801 Уровень 35
19 февраля 2026
присоединяюсь))
Vany_int64 Уровень 112
30 марта 2026
Вы меня убили
Gans Electro Уровень 59
3 августа 2026
В итоге только один дошел до 60 уровней
rwnmei Уровень 57
31 августа 2026
бывает
Gans Electro Уровень 59
30 октября 2026
Ладно другой купил University все же далеко пойдет, третий пока не объявился