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