CodeGym /Kursy /JAVA 25 SELF /Strumienie prymitywne i koszt opakowywania (boxing)

Strumienie prymitywne i koszt opakowywania (boxing)

JAVA 25 SELF
Poziom 33 , Lekcja 0
Dostępny

1. Problem: dlaczego zwykłe Stream nie zawsze są wydajne

Gdy pracujesz z kolekcjami liczb w Javie i używasz zwykłych strumieni (Stream<Integer>, Stream<Double>), pod spodem zachodzi „opakowywanie” (boxing) i „rozpakowywanie” (unboxing) wartości prymitywnych do typów opakowujących (Integer, Double itd.). To wygodne, ale nie zawsze wydajne:

  • Boxing — przekształcenie prymitywu (int) w obiekt (Integer).
  • Unboxing — operacja odwrotna: z obiektu do prymitywu.

Problem:
Boxing/unboxing to dodatkowe operacje i zużycie pamięci. W „gorących” (często wywoływanych) miejscach programu może to prowadzić do zauważalnego spadku wydajności, zwłaszcza gdy przetwarzasz duże tablice liczb.

Przykład:

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

Tutaj każda liczba to obiekt Integer, a nie prymityw int.

2. Strumienie prymitywne: IntStream, LongStream, DoubleStream

Aby uniknąć zbędnego boxing/unboxing, w Javie są strumienie prymitywne:

  • IntStream — dla int
  • LongStream — dla long
  • DoubleStream — dla double

Działają wyłącznie na prymitywach i nie tworzą zbędnych obiektów opakowujących.

Jak utworzyć strumień prymitywny?

Z tablicy:

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

Za pomocą range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 włącznie

Generowanie liczb losowych:

new Random().ints(5, 0, 100)    // 5 losowych int od 0 do 99

Przykład: sumowanie elementów tablicy

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

3. Konwersja między Stream<T> a strumieniami prymitywnymi

Czasem masz zwykły strumień, a czasem — prymitywny. Do konwersji używa się specjalnych metod:

  • mapToInt, mapToLong, mapToDouble — konwertują zwykły strumień na prymitywny.
  • boxed() — zamienia strumień prymitywny z powrotem na strumień obiektów.

Przykład:

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

W drugą stronę:

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

Uwaga:
boxed() to operacja odwrotna, która znów tworzy typy opakowujące (Integer, Double itd.). Jeśli zależy ci na maksymalnej wydajności — unikaj jej w „gorących” miejscach.

4. Sumowanie i podsumowania: sum, average, min, max, summaryStatistics

Strumienie prymitywne mają wygodne metody dla agregacji:

  • sum() — suma wszystkich elementów
  • average() — średnia (zwraca OptionalDouble)
  • min(), max() — minimum i maksimum (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — zwraca obiekt z pełną statystyką (suma, średnia, minimum, maksimum, liczba elementów)

Przykład:

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

Porównanie z Collectors:
Dla zwykłych strumieni można użyć Collectors.summarizingInt, ale to zwykle mniej wydajne niż metody strumieni prymitywnych:

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

5. Unikamy autoboxingu: gdzie to ma znaczenie i jak to zmierzyć

Gdzie to jest krytyczne?

  • W pętlach i strumieniach, które przetwarzają duże ilości liczb (np. obróbka tablic, statystyka, matematyka, parsowanie danych).
  • W „gorących” miejscach — kod często wywoływany, wpływający na wydajność.

Dlaczego to ważne?

  • Za każdym razem, gdy zachodzi boxing, tworzony jest nowy obiekt‑opakowanie (Integer, Double itd.).
  • Zwiększa to obciążenie garbage collectora (GC).
  • W mikrobenchmarkach różnica może być wielokrotna!

Jak to zmierzyć?
Do rzetelnego porównania używa się frameworka JMH (Java Microbenchmark Harness). Pozwala on uczciwie porównać wydajność kodu z boxingiem i bez.

Przykład (pseudokod):

@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);
}

Drugi wariant będzie działał wolniej ze względu na ciągłe tworzenie obiektów Integer.

Wniosek:
Jeśli zależy ci na wydajności — używaj strumieni prymitywnych!

6. Kiedy strumienie prymitywne naprawdę pomagają, a kiedy nie warto komplikować

Używaj strumieni prymitywnych, jeśli:

  • Pracujesz z dużymi tablicami/kolekcjami liczb.
  • Musisz szybko policzyć sumę, średnią, minimum, maksimum.
  • Piszesz kod, w którym liczy się każda milisekunda (np. przetwarzanie danych w czasie rzeczywistym).

Można sobie odpuścić, jeśli:

  • Kolekcja jest niewielka (kilka–kilkadziesiąt elementów).
  • Kod staje się zbyt skomplikowany przez konwersje między typami.
  • Wydajność nie jest krytyczna (np. obsługa danych wejściowych użytkownika).

Przykład:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Można, ale zwykłe reduce też wystarczy

Wskazówka: nie zamieniaj kodu w „dżunglę” dla mikroskopijnej optymalizacji. Używaj strumieni prymitywnych tam, gdzie to rzeczywiście uzasadnione.

7. OptionalInt, OptionalDouble: bezpieczne pobieranie wyników

Metody min(), max(), average() w strumieniach prymitywnych zwracają nie po prostu liczbę, lecz „opakowanie” — OptionalInt, OptionalDouble itd. To po to, by bezpiecznie obsługiwać puste strumienie (np. gdy tablica jest pusta).

Przykład:

int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
    System.out.println("Minimum: " + min.getAsInt());
} else {
    System.out.println("Tablica jest pusta!");
}

Porównanie ze zwykłym Optional:

  • OptionalInt — dla int
  • OptionalDouble — dla double
  • OptionalLong — dla long

Dlaczego nie zwracać po prostu 0?
Ponieważ 0 może być poprawną wartością, a pusty strumień to osobny przypadek.

Przykład z average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // jeśli pusto — zwróci NaN

8. Praktyka: przykłady użycia strumieni prymitywnych

Przykład 1: Suma kwadratów liczb od 1 do 1000

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

Przykład 2: Filtrowanie i zliczanie liczb parzystych

int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
                   .filter(x -> x % 2 == 0)
                   .count();
System.out.println("Liczb parzystych: " + count);

Przykład 3: Porównanie ze zwykłym 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");

Przy dużych wolumenach różnica będzie zauważalna.

9. Typowe błędy przy pracy ze strumieniami prymitywnymi

Błąd nr 1: Zapomniano o boxingu przy konwersji typów.
Jeśli używasz boxed(), pamiętaj, że to znów tworzy obiekty opakowujące. Nie używaj bez potrzeby.

Błąd nr 2: Używanie strumieni prymitywnych do obiektów.
IntStream działa tylko na int, a nie na obiektach. Jeśli potrzebujesz pracować na obiektach — użyj zwykłego Stream<T>.

Błąd nr 3: Ignorowanie OptionalInt/OptionalDouble.
Jeśli wywołujesz min(), max(), average() — zawsze sprawdzaj, czy wynik istnieje (isPresent()), w przeciwnym razie otrzymasz wyjątek.

Błąd nr 4: Zbyt skomplikowane konwersje między Stream<T> a IntStream.
Jeśli kod staje się nieczytelny przez ciągłe mapToInt()boxed()mapToDouble() — być może warto uprościć logikę.

Błąd nr 5: Oczekiwanie „magicznego” przyspieszenia na małych kolekcjach.
Dla małych list różnica między Stream a IntStream jest minimalna. Nie warto komplikować kodu dla mikroskopijnej oszczędności.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION