CodeGym /Kurse /JAVA 25 SELF /Primitive Streams und die Kosten des Boxings (boxing)

Primitive Streams und die Kosten des Boxings (boxing)

JAVA 25 SELF
Level 33 , Lektion 0
Verfügbar

1. Problem: warum gewöhnliche Stream nicht immer effizient sind

Wenn Sie in Java mit Sammlungen von Zahlen arbeiten und gewöhnliche Streams (Stream<Integer>, Stream<Double>) verwenden, findet unter der Haube „Boxing“ (boxing) und „Unboxing“ (unboxing) der primitiven Werte in Wrapper-Objekte (Integer, Double usw.) statt. Das ist bequem, aber nicht immer effizient:

  • Boxing — die Umwandlung eines Primitivs (int) in ein Objekt (Integer).
  • Unboxing — die umgekehrte Operation: vom Objekt zum Primitiv.

Problem:
Boxing/Unboxing sind zusätzliche Operationen und verbrauchen Speicher. In „heißen“ (häufig aufgerufenen) Stellen des Programms kann das zu spürbaren Leistungseinbußen führen, besonders wenn Sie große Arrays von Zahlen verarbeiten.

Beispiel:

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

Hier ist jede Zahl ein Integer-Objekt und kein primitives int.

2. Primitive Streams: IntStream, LongStream, DoubleStream

Um überflüssiges Boxing/Unboxing zu vermeiden, gibt es in Java primitive Streams:

  • IntStream — für int
  • LongStream — für long
  • DoubleStream — für double

Sie arbeiten ausschließlich mit Primitiven und erzeugen keine unnötigen Wrapper-Objekte.

Wie erstellt man einen primitiven Stream?

Aus einem Array:

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

Mit range/rangeClosed:

IntStream.range(0, 10)          // 0..9
IntStream.rangeClosed(1, 5)     // 1..5 einschließlich

Erzeugen von Zufallszahlen:

new Random().ints(5, 0, 100)    // 5 zufällige int von 0 bis 99

Beispiel: Summe der Array-Elemente

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

3. Umwandlung zwischen Stream<T> und primitiven Streams

Manchmal haben Sie einen gewöhnlichen Stream, manchmal einen primitiven. Für die Umwandlung gibt es spezielle Methoden:

  • mapToInt, mapToLong, mapToDouble — wandeln einen gewöhnlichen Stream in einen primitiven um.
  • boxed() — verwandelt einen primitiven Stream zurück in einen Stream von Objekten.

Beispiel:

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

Rückrichtung:

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

Achtung:
boxed() ist die Umkehrung; sie erzeugt wieder Wrapper-Objekte (Integer, Double usw.). Wenn Ihnen maximale Performance wichtig ist — vermeiden Sie dies in „heißen“ Stellen.

4. Summen und Statistiken: sum, average, min, max, summaryStatistics

Primitive Streams bieten bequeme Aggregationsmethoden:

  • sum() — Summe aller Elemente
  • average() — Durchschnitt (liefert OptionalDouble)
  • min(), max() — Minimum und Maximum (OptionalInt, OptionalLong, OptionalDouble)
  • summaryStatistics() — liefert ein Objekt mit vollständiger Statistik (Summe, Durchschnitt, Minimum, Maximum, Anzahl)

Beispiel:

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

Vergleich mit Collectors:
Für normale Streams kann man Collectors.summarizingInt verwenden, aber das ist oft weniger effizient als die Methoden der primitiven Streams:

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

5. Autoboxing vermeiden: wo es wichtig ist und wie man es misst

Wo ist es kritisch?

  • In Schleifen und Streams, die große Mengen an Zahlen verarbeiten (z. B. Array-Verarbeitung, Statistik, Mathematik, Parsing von Daten).
  • In „heißen“ Stellen — Code, der häufig aufgerufen wird und die Performance beeinflusst.

Warum ist das wichtig?

  • Jedes Mal, wenn Boxing passiert, wird ein neues Wrapper-Objekt (Integer, Double usw.) erstellt.
  • Das erhöht die Last für den Garbage Collector (GC).
  • In Mikrobenchmarks kann der Unterschied um ein Vielfaches betragen!

Wie messen?
Für einen fairen Vergleich verwendet man das Framework JMH (Java Microbenchmark Harness). Es ermöglicht, die Performance von Code mit und ohne Boxing fair zu vergleichen.

Beispiel (Pseudocode):

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

Die zweite Variante wird wegen der ständigen Erstellung von Integer-Objekten langsamer laufen.

Fazit:
Wenn Ihnen Performance wichtig ist — verwenden Sie primitive Streams!

6. Wann primitive Streams wirklich helfen, und wann man es nicht übertreiben sollte

Verwenden Sie primitive Streams, wenn:

  • Sie mit großen Arrays/Collections von Zahlen arbeiten.
  • Sie schnell Summe, Durchschnitt, Minimum, Maximum berechnen müssen.
  • Sie Code schreiben, bei dem jede Millisekunde zählt (z. B. Datenverarbeitung in Echtzeit).

Man kann es auch lassen, wenn:

  • Die Collection klein ist (einige Dutzend Elemente).
  • Der Code wegen der Umwandlungen zwischen Typen zu komplex wird.
  • Die Performance nicht kritisch ist (z. B. Verarbeitung von Benutzereingaben).

Beispiel:

List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // Kann man machen, aber ein gewöhnliches reduce geht auch

Tipp: Verwandeln Sie den Code nicht in einen „Dschungel“ wegen mikroskopischer Optimierungen. Verwenden Sie primitive Streams dort, wo es wirklich gerechtfertigt ist.

7. OptionalInt, OptionalDouble: Ergebnisse sicher auslesen

Die Methoden min(), max(), average() der primitiven Streams liefern nicht einfach eine Zahl, sondern einen „Wrapper“ — OptionalInt, OptionalDouble usw. Das ist nötig, um leere Streams sicher zu behandeln (z. B. wenn das Array leer ist).

Beispiel:

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

Vergleich mit gewöhnlichem Optional:

  • OptionalInt — für int
  • OptionalDouble — für double
  • OptionalLong — für long

Warum nicht einfach 0 zurückgeben?
Weil 0 ein gültiger Wert sein kann, während ein leerer Stream ein eigener Fall ist.

Beispiel mit average:

double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // wenn leer — gibt NaN zurück

8. Praxis: Beispiele für die Verwendung primitiver Streams

Beispiel 1: Summe der Quadrate von 1 bis 1000

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

Beispiel 2: Filtern und Zählen gerader Zahlen

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

Beispiel 3: Vergleich mit einem gewöhnlichen 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");

Bei großen Datenmengen wird der Unterschied spürbar sein.

9. Typische Fehler im Umgang mit primitiven Streams

Fehler Nr. 1: Boxing bei Typumwandlungen vergessen.
Wenn Sie boxed() verwenden, bedenken Sie, dass dabei wieder Wrapper-Objekte entstehen. Nicht ohne Not verwenden.

Fehler Nr. 2: Primitive Streams für Objekte verwenden.
IntStream arbeitet nur mit int, nicht mit Objekten. Wenn Sie mit Objekten arbeiten müssen — verwenden Sie einen normalen Stream<T>.

Fehler Nr. 3: OptionalInt/OptionalDouble ignorieren.
Wenn Sie min(), max(), average() aufrufen — prüfen Sie immer, ob ein Ergebnis vorhanden ist (isPresent()), sonst riskieren Sie eine Ausnahme.

Fehler Nr. 4: Zu komplexe Umwandlungen zwischen Stream<T> und IntStream.
Wenn der Code wegen ständiger mapToInt()boxed()mapToDouble() unlesbar wird — sollten Sie die Logik vereinfachen.

Fehler Nr. 5: Erwartung eines „magischen“ Geschwindigkeitsschubs bei kleinen Collections.
Bei kleinen Listen ist der Unterschied zwischen Stream und IntStream minimal. Verkomplizieren Sie den Code nicht für mikroskopische Einsparungen.

1
Aufgabe
JAVA 25 SELF, Level 33, Lektion 0
Gesperrt
Tägliche Einnahmen des Shops 💰
Tägliche Einnahmen des Shops 💰
1
Aufgabe
JAVA 25 SELF, Level 33, Lektion 0
Gesperrt
Analyse der Länge von Zaubernamen für die Magische Akademie ✨
Analyse der Länge von Zaubernamen für die Magische Akademie ✨
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION