CodeGym /Kurse /JAVA 25 SELF /Analyse typischer Fehler beim Arbeiten mit Streams

Analyse typischer Fehler beim Arbeiten mit Streams

JAVA 25 SELF
Level 32, Lektion 4
Verfügbar

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);
1
Aufgabe
JAVA 25 SELF, Level 32, Lektion 4
Gesperrt
Gefährliche Datenbereinigung mit möglichen "leeren" Einträgen 😼
Gefährliche Datenbereinigung mit möglichen "leeren" Einträgen 😼
1
Aufgabe
JAVA 25 SELF, Level 32, Lektion 4
Gesperrt
Magische einmalig verwendbare Schriftrolle mit Zaubern 📜
Magische einmalig verwendbare Schriftrolle mit Zaubern 📜
1
Umfrage/Quiz
Vereinigungen und Projektionen, Level 32, Lektion 4
Nicht verfügbar
Vereinigungen und Projektionen
Stream API: Vereinigungen und Projektionen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION