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) + " мс");

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

На великих обсягах різниця буде помітною.

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 мінімальна. Не варто ускладнювати код заради мікроскопічної економії.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ