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 мінімальна. Не варто ускладнювати код заради мікроскопічної економії.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ