1. Fehler: erneute Verwendung eines Streams
Ein Stream in Java ist ein Einweg-Werkzeug. Sobald Sie eine terminale Operation ausgeführt haben (zum Beispiel collect(), forEach(), count()), wird der Stream geschlossen und ist für die weitere Verwendung ungeeignet.
Fehlerbeispiel
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // Alles in Ordnung
stream.forEach(System.out::println); // Wirft IllegalStateException!
Ausgabe:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
So vermeiden Sie das
Wenn Sie mehrere Operationen ausführen müssen – erstellen Sie jedes Mal einen neuen Stream:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
Lifehack: Ein Stream ist wie ein Einwegbecher in der Mensa: benutzt – wegwerfen, für das nächste Getränk einen neuen nehmen.
2. Fehler: Änderung der Sammlung während des Streams
Wenn Sie die ursprüngliche Sammlung während der Iteration per Stream modifizieren, erhalten Sie eine ConcurrentModificationException. Das ist ein häufiger Schmerzpunkt beim Filtern und Entfernen von Elementen.
Fehlerbeispiel
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // BUMM!
}
});
Ergebnis:
Exception in thread "main" java.util.ConcurrentModificationException
So geht's richtig
Verwenden Sie Methoden, die die Sammlung während der Iteration nicht modifizieren, oder sammeln Sie das Ergebnis in eine neue Sammlung:
names.removeIf(name -> name.startsWith("A")); // OK, removeIf ist sicher
// Oder mit dem Stream-API:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
Merken Sie sich: Streams mögen Sauberkeit – fassen Sie die Quellsammlung nicht an, solange sie von Streams „verzehrt“ wird.
3. Fehler: fehlendes limit bei unendlichen Streams
Die Methoden Stream.iterate und Stream.generate können unendliche Streams erzeugen. Wenn man vergisst, ihre Größe mit limit zu begrenzen, kann das Programm ewig hängen (oder bis zum OutOfMemoryError).
Fehlerbeispiel
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // Das Programm wird niemals fertig!
So geht's richtig
Begrenzen Sie den Stream mit limit:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Gibt die Zahlen von 0 bis 9 aus
Tipp: Unendliche Streams erinnern an eine Serie ohne Finale: Jemand muss stoppen – am besten Sie!
4. Fehler: falscher Umgang mit null
Das Stream-API mag null nicht. Befindet sich null in der Sammlung, können viele Operationen eine NullPointerException werfen, insbesondere wenn Sie Methoden auf Elementen aufrufen.
Fehlerbeispiel
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // BUMM! bei null
.forEach(System.out::println);
Ergebnis:
Exception in thread "main" java.lang.NullPointerException
So geht's richtig
Filtern Sie null-Werte vorher heraus:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Arbeiten mit Optional
Wenn Sie ein Stream<Optional<T>> haben, verwenden Sie flatMap(Optional::stream) (Java 9+) zum „Entpacken“:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Gibt nur "A" und "B" aus
Tipp: Vermeiden Sie es, null zu Sammlungen hinzuzufügen. Wenn es gar nicht anders geht, filtern Sie sie direkt am Anfang heraus.
5. Fehler: Verlust der Elementreihenfolge
Einige Operationen (zum Beispiel flatMap, concat) können die Reihenfolge der Elemente beeinflussen, insbesondere wenn Sie mit parallelen Streams arbeiten oder verschiedene Sammlungen zusammenführen.
Fehlerbeispiel
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — die Reihenfolge bleibt erhalten
// Aber wenn man parallele Streams verwendet:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // Die Reihenfolge kann unerwartet sein!
So geht's richtig
Wenn die Reihenfolge wichtig ist – vermeiden Sie parallele Streams oder verwenden Sie Methoden, die die Reihenfolge garantieren (forEachOrdered):
par.forEachOrdered(System.out::print);
Fakt: Das Stream-API bewahrt standardmäßig die Reihenfolge, sofern Sie sie nicht selbst „zerstört“ haben.
6. Fehler: ineffizientes Vereinen von Sammlungen
Für Mengenoperationen verwenden Sie besser Set statt List, um Duplikate zu vermeiden und die Leistung zu verbessern. Ebenfalls ineffizient sind verschachtelte Schleifen, wo man flatMap nutzen kann.
Fehlerbeispiel
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// Ineffizient:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // contains bei List — O(n)
union.add(s);
}
}
// Effizient mit Set und Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
Oder für die Differenz:
// Ineffizient:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, aber wenn Sie es per stream und list.contains gemacht hätten — wäre es langsam gewesen
// Effizient:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
Tipp: Für einzigartige Elemente verwenden Sie immer ein Set. Für große Sammlungen – vermeiden Sie verschachtelte Schleifen, bevorzugen Sie stream + flatMap.
7. Fehler: doppelte Verwendung terminaler Operationen
Manchmal möchte man einen Stream in zwei Zweige „aufteilen“, zum Beispiel Elemente in eine Liste sammeln und parallel ihre Anzahl zählen. Aber ein Stream ist „einmalig“, und die zweite Operation führt zu einem Fehler.
Fehlerbeispiel
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // BUMM! IllegalStateException
So geht's richtig
Wenn Sie den Stream mehrfach verwenden müssen – sammeln Sie ihn zuerst in eine Sammlung:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
Tipp: Wenn Sie „das Beste aus beiden Welten“ wollen – sammeln Sie zuerst in eine Sammlung.
8. Fehler: vergessenes close() bei Ressourcen
Wenn Sie Streams verwenden, die mit Ressourcen verbunden sind (zum Beispiel Files.lines(Path)), vergessen Sie nicht, sie nach der Verwendung zu schließen, um Lecks zu vermeiden.
Fehlerbeispiel
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, aber die Ressource ist nicht geschlossen!
So geht's richtig
Verwenden Sie try-with-resources:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
Tipp: File-Streams sind wie ein Wasserkocher: aufkochen – ausschalten!
9. Fehler: inkorrekte Verarbeitung von Optional und null in flatMap
Bei der Arbeit mit Sammlungen wie List<Optional<T>> verwenden viele map(Optional::get), was zu Ausnahmen führt, wenn das Optional leer ist.
Fehlerbeispiel
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // BUMM! NoSuchElementException
.forEach(System.out::println);
So geht's richtig
Verwenden Sie flatMap(Optional::stream) (Java 9+):
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // Nur "A"
Tipp: Optional ist nicht nur ein Wrapper, sondern ein Werkzeug für den sicheren Umgang mit fehlenden Werten.
10. Fehler: falsch implementiertes equals/hashCode
Mengenoperationen (Set, distinct, Vereinigung, Schnittmenge) hängen von der korrekten Implementierung der Methoden equals und hashCode ab.
Fehlerbeispiel
class Student {
String name;
int age;
// equals und hashCode fehlen!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, obwohl sie gleich sein sollten!
So geht's richtig
Überschreiben Sie equals und hashCode für Ihre Klassen, wenn Sie sie in Set-, Map-Sammlungen oder für distinct-Operationen verwenden möchten.
11. Fehler: Verwendung eines Streams nach collect
Nach einer terminalen Operation (zum Beispiel collect) wird der Stream geschlossen. Jeder Versuch, mit demselben Stream weiterzuarbeiten, führt zu einem Fehler.
Fehlerbeispiel
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!
So geht's richtig
Erstellen Sie einen neuen Stream aus der Sammlung:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. Fehler: falsche Verwendung von parallelStream
Viele denken, dass parallelStream() die Verarbeitung immer beschleunigt. In Wahrheit kann Parallelität bei kleinen Sammlungen oder einfachen Operationen aufgrund des Verwaltungs-Overheads für Threads sogar verlangsamen.
Fehlerbeispiel
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Kann langsamer sein als ein normaler stream()
So geht's richtig
Verwenden Sie parallelStream nur für wirklich große Sammlungen oder rechenintensive Aufgaben, insbesondere wenn die Operationen rein (ohne Nebenwirkungen) und assoziativ sind.
13. Fehler: vergessener terminaler Operator
Zwischenoperationen (map, filter, flatMap usw.) werden erst ausgeführt, wenn ein terminaler Operator aufgerufen wird (collect, forEach, count usw.).
Fehlerbeispiel
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Es passiert nichts!
So geht's richtig
Rufen Sie unbedingt einen terminalen Operator auf:
stream.forEach(System.out::println);
GO TO FULL VERSION