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.
GO TO FULL VERSION