CodeGym /Kursy /JAVA 25 SELF /Analiza typowych błędów podczas pracy ze strumieniami

Analiza typowych błędów podczas pracy ze strumieniami

JAVA 25 SELF
Poziom 32 , Lekcja 4
Dostępny

1. Błąd: ponowne użycie strumienia

Strumień (Stream) w Javie to narzędzie jednorazowego użytku. Gdy tylko wykonasz operację terminalną (np. collect(), forEach(), count()), strumień zostaje zamknięty i nie nadaje się do dalszego użycia.

Przykład błędu

Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // Wszystko w porządku
stream.forEach(System.out::println); // Rzuci IllegalStateException!

Wniosek:

a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

Jak uniknąć

Jeśli chcesz wykonać kilka operacji — twórz za każdym razem nowy strumień:

List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK

Lifehack: Strumień jest jak jednorazowy kubek w stołówce: zjadłeś — wyrzucasz, po kolejny napój bierzesz nowy.

2. Błąd: modyfikacja kolekcji podczas przetwarzania strumieniem

Jeśli modyfikujesz źródłową kolekcję w trakcie iteracji za pomocą strumienia, dostaniesz ConcurrentModificationException. To częsty ból przy filtrowaniu i usuwaniu elementów.

Przykład błędu

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // BUM!
    }
});

Wynik:

Exception in thread "main" java.util.ConcurrentModificationException

Jak prawidłowo

Używaj metod, które nie modyfikują kolekcji podczas iteracji, albo zbieraj wynik do nowej kolekcji:

names.removeIf(name -> name.startsWith("A")); // OK, removeIf jest bezpieczny

// Albo z użyciem Stream API:
List<String> filtered = names.stream()
    .filter(name -> !name.startsWith("A"))
    .collect(Collectors.toList());

Zapamiętaj: Strumienie lubią czystość — nie dotykaj kolekcji źródłowej, gdy jest „zjadana” przez strumienie.

3. Błąd: zapomniany limit przy strumieniach nieskończonych

Metody Stream.iterate i Stream.generate mogą tworzyć strumienie nieskończone. Jeśli zapomnisz ograniczyć ich rozmiar metodą limit, program może zawiesić się na zawsze (lub do OutOfMemoryError).

Przykład błędu

Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // Program nigdy się nie zakończy!

Jak prawidłowo

Ograniczaj strumień metodą limit:

Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // Wypisze liczby od 0 do 9

Wskazówka: Strumienie nieskończone przypominają serial bez finałowego odcinka: ktoś musi to zatrzymać — najlepiej Ty!

4. Błąd: nieprawidłowa praca z null

Stream API nie lubi null. Jeśli w kolekcji znajdzie się null, wiele operacji może rzucić NullPointerException, zwłaszcza gdy wywołujesz metody na elementach.

Przykład błędu

List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
    .map(String::toUpperCase) // BUM! Na null
    .forEach(System.out::println);

Wynik:

Exception in thread "main" java.lang.NullPointerException

Jak prawidłowo

Filtruj wartości null wcześniej:

names.stream()
    .filter(Objects::nonNull)
    .map(String::toUpperCase)
    .forEach(System.out::println);

Praca z Optional

Jeśli masz Stream<Optional<T>>, użyj flatMap(Optional::stream) (Java 9+) do „rozpakowania”:

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // Wypisze tylko "A" i "B"

Wskazówka: Staraj się nie dodawać null do kolekcji. Jeśli już musisz, filtruj je na starcie.

5. Błąd: utrata kolejności elementów

Niektóre operacje (np. flatMap, concat) mogą wpływać na kolejność elementów, zwłaszcza jeśli pracujesz ze strumieniami równoległymi lub łączysz różne kolekcje.

Przykład błędu

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 — kolejność pozostaje zachowana

// Ale jeśli użyjesz strumieni równoległych:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // Kolejność może być nieoczekiwana!

Jak prawidłowo

Jeśli kolejność jest ważna — unikaj strumieni równoległych lub używaj metod gwarantujących kolejność (forEachOrdered):

par.forEachOrdered(System.out::print);

Fakt: Stream API domyślnie zachowuje kolejność, o ile sam jej nie „zepsujesz”.

6. Błąd: nieefektywne łączenie kolekcji

Do operacji na zbiorach lepiej używać Set, a nie List, aby uniknąć duplikatów i poprawić wydajność. Nieefektywne jest też pisanie zagnieżdżonych pętli tam, gdzie można użyć flatMap.

Przykład błędu

List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");

// Nieefektywnie:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
    if (!union.contains(s)) { // contains w List — O(n)
        union.add(s);
    }
}

// Efektywnie z Set i Stream API:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
    .collect(Collectors.toSet());

Albo dla różnicy:

// Nieefektywnie:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK, ale gdybyś robił to przez stream i list.contains — byłoby wolno

// Efektywnie:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
    .filter(s -> !set2.contains(s))
    .collect(Collectors.toList());

Wskazówka: Do pracy z unikalnymi elementami zawsze używaj Set. Dla dużych kolekcji — unikaj zagnieżdżonych pętli, preferuj stream + flatMap.

7. Błąd: ponowne użycie operacji terminalnych

Czasem chcemy „rozdzielić” strumień na dwie gałęzie, np. zebrać elementy do listy i równolegle policzyć ich liczbę. Ale strumień jest „jednorazowy” i druga operacja spowoduje błąd.

Przykład błędu

Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // BUM! IllegalStateException

Jak prawidłowo

Jeśli potrzebujesz użyć strumienia kilka razy — najpierw zbierz go do kolekcji:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();

Wskazówka: Jeśli chcesz zjeść ciastko i mieć ciastko — najpierw zbierz do kolekcji.

8. Błąd: zapomniane wywołanie close() dla zasobów

Jeśli używasz strumieni powiązanych z zasobami (np. Files.lines(Path)), nie zapominaj zamykać ich po użyciu, aby uniknąć wycieków.

Przykład błędu

Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK, ale zasób nie został zamknięty!

Jak prawidłowo

Używaj try-with-resources:

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.forEach(System.out::println);
}

Wskazówka: Strumienie z plików są jak czajnik: zagotujesz — wyłącz!

9. Błąd: nieprawidłowa obsługa Optional i null w flatMap

Pracując z kolekcjami typu List<Optional<T>> wielu używa map(Optional::get), co prowadzi do wyjątków, gdy Optional jest pusty.

Przykład błędu

List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
    .map(Optional::get) // BUM! NoSuchElementException
    .forEach(System.out::println);

Jak prawidłowo

Używaj flatMap(Optional::stream) (Java 9+):

optionals.stream()
    .flatMap(Optional::stream)
    .forEach(System.out::println); // Tylko "A"

Wskazówka: Optional to nie tylko „pudełko”, ale narzędzie do bezpiecznej pracy z brakiem wartości.

10. Błąd: błędnie zaimplementowane equals/hashCode

Operacje na zbiorach (Set, distinct, suma, część wspólna) zależą od poprawnej implementacji metod equals i hashCode.

Przykład błędu

class Student {
    String name;
    int age;
    // Brak equals i hashCode!
}

Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2, chociaż powinny być takie same!

Jak prawidłowo

Nadpisuj equals i hashCode w swoich klasach, jeśli planujesz używać ich w kolekcjach Set, Map lub do operacji distinct.

11. Błąd: użycie stream po collect

Po operacji terminalnej (np. collect) strumień jest zamknięty. Każda próba kontynuacji pracy na tym samym strumieniu skończy się błędem.

Przykład błędu

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!

Jak prawidłowo

Twórz nowy strumień z kolekcji:

list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);

12. Błąd: nieprawidłowe użycie parallelStream

Wielu myśli, że parallelStream() zawsze przyspiesza przetwarzanie. W rzeczywistości, dla małych kolekcji lub prostych operacji, równoległość może tylko spowolnić wykonanie ze względu na narzuty związane z zarządzaniem wątkami.

Przykład błędu

List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // Może być wolniejsze niż zwykły stream()

Jak prawidłowo

Używaj parallelStream tylko dla naprawdę dużych kolekcji lub ciężkich obliczeń, szczególnie gdy operacje są czyste (bez efektów ubocznych) i łączne (asocjatywne).

13. Błąd: zapomniany operator terminalny

Operacje pośrednie (map, filter, flatMap itd.) nie są wykonywane, dopóki nie zostanie wywołany operator terminalny (collect, forEach, count itd.).

Przykład błędu

Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// Nic się nie stanie!

Jak prawidłowo

Koniecznie wywołaj operator terminalny:

stream.forEach(System.out::println);
1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 4
Niedostępne
Niebezpieczne czyszczenie bazy danych z możliwymi "pustymi" rekordami 😼
Niebezpieczne czyszczenie bazy danych z możliwymi "pustymi" rekordami 😼
1
Zadanie
JAVA 25 SELF, poziom 32, lekcja 4
Niedostępne
Magiczny zwój z jednorazowym użyciem zaklęć 📜
Magiczny zwój z jednorazowym użyciem zaklęć 📜
1
Ankieta/quiz
Unie i projekcje, poziom 32, lekcja 4
Niedostępny
Unie i projekcje
Stream API: unie i projekcje
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION