1. Konwersja List → Set i z powrotem
Przypomnijmy, po co w ogóle potrzebne są przekształcenia kolekcji. Często w realnych zadaniach trzeba:
- Uzyskać z listy unikalne elementy (np. lista e‑maili → zbiór unikalnych adresów).
- Zbudować mapę (Map), na przykład z listy imion utworzyć mapę „imię → długość imienia”.
- Połączyć elementy w jeden ciąg (np. dla ładnego wyświetlenia).
Kiedyś trzeba było pisać sporo kodu z pętlami, warunkami i tymczasowymi kolekcjami. Dzięki Stream API wszystko stało się prostsze i… bardziej eleganckie!
Przykład: uzyskać zbiór unikalnych imion z listy
Załóżmy, że mamy listę imion (może ktoś w Waszym programie wpisał się dwa razy — bywa!):
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Nasze zadanie — uzyskać kolekcję, w której każde imię występuje tylko raz, czyli zbiór (Set). Z pomocą Stream API robi się to dosłownie w jednym wierszu:
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Wynik:
[Mariya, Ivan, Anna, Sergey]
(Kolejność w Set nie jest gwarantowana — nie dziwcie się, jeśli u Was będzie inna.)
A jeśli trzeba odwrotnie: Set → List?
Czasem trzeba odwrotnie — zamienić zbiór na listę (np. żeby posortować lub uzyskać dostęp po indeksie):
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Konwersja do Map: Collectors.toMap()
Przykład: z listy imion uzyskać Map „imię → długość imienia”
Czasem chce się być nie tylko programistą, ale i kartografem — budować mapy! Spróbujmy:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // klucz — samo imię
name -> name.length() // wartość — długość imienia
));
System.out.println(nameToLength);
Wynik:
{Mariya=6, Ivan=4, Anna=4, Sergey=6}
Ważny moment: duplikaty kluczy
Jeśli w źródłowej liście są takie same imiona, to przy próbie złożenia ich w Map wystąpi błąd IllegalStateException: Duplicate key. Java nie lubi, gdy próbujecie włożyć dwie wartości pod ten sam klucz.
Jak obsłużyć duplikaty?
Można wskazać, co robić przy kolizji kluczy — na przykład zostawić pierwszą wartość albo ostatnią:
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // zachować pierwszą wartość
));
System.out.println(nameToLength);
Teraz program nie wyrzuci wyjątku, a do Map trafi tylko pierwsze wystąpienie każdego imienia.
Przykład: Map z obiektami
Nieco utrudnijmy: mamy listę użytkowników i chcemy zbudować Map „imię → użytkownik”:
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Przykładowa lista użytkowników
List<User> users = List.of(
new User("Anna", 25),
new User("Sergey", 30),
new User("Mariya", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Wynik:
{Mariya=Mariya (22), Anna=Anna (25), Sergey=Sergey (30)}
3. Łączenie w ciąg: Collectors.joining()
Czasem nie chcemy po prostu zebrać kolekcji, lecz stworzyć ładny tekst do wyświetlenia użytkownikowi lub do logów. Na przykład zebrać wszystkie imiona po przecinku:
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Wynik:
Anna, Sergey, Mariya, Ivan
Można dodać prefiks i sufiks
String result = names.stream()
.collect(Collectors.joining(", ", "Lista: [", "]"));
System.out.println(result);
Wynik:
Lista: [Anna, Sergey, Mariya, Ivan]
4. Operacje terminalne: forEach, collect, count, anyMatch, allMatch, noneMatch
Metoda forEach
Z forEach jesteśmy już dobrze zaznajomieni: ta operacja wykonuje działanie dla każdego elementu strumienia.
names.stream().forEach(name -> System.out.println("Cześć, " + name + "!"));
Metoda collect
Zbiera elementy do kolekcji, ciągu lub innej struktury. Najczęstsza operacja — zbieranie do List albo Set za pomocą Collectors.toList() i Collectors.toSet().
Metoda count
Zlicza liczbę elementów w strumieniu.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Liczba imion dłuższych niż 4 litery: " + count);
Metody anyMatch, allMatch, noneMatch
Sprawdzają, czy warunek spełniony jest choć dla jednego elementu (anyMatch), dla wszystkich (allMatch) lub dla żadnego (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Czy istnieje krótkie imię? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Czy wszystkie imiona są dłuższe niż 3 litery? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Ivan"));
System.out.println("Czy nie ma Ivana? " + noneIvan);
Wynik:
Czy istnieje krótkie imię? false
Czy wszystkie imiona są dłuższe niż 3 litery? true
Czy nie ma Ivana? false
5. Operacje terminalne i pośrednie: utrwalmy pojęcia
Operacje pośrednie (filter, map, distinct, sorted, limit, skip, peek) — zwracają nowy Stream, można budować łańcuchy.
Operacje terminalne (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — kończą strumień, dalszego wyniku nie będzie!
Przykład łańcucha:
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Wynik:
[ANNA, IVAN, MARIYA, SERGEY]
6. Typowe błędy przy przekształcaniu kolekcji przez Stream
Błąd nr 1: Brak obsługi duplikatów kluczy w toMap
Jeśli w źródłowej kolekcji występują zduplikowane klucze, a używacie Collectors.toMap() bez jawnego operatora łączenia, program wyrzuci wyjątek. W takich przypadkach zawsze podawajcie funkcję scalania:
// Zachować ostatnią wartość
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Błąd nr 2: Używanie forEach zamiast collect
Czasem początkujący próbują „zebrać” kolekcję za pomocą forEach, na przykład:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Działa, ale to nie jest podejście w stylu Stream!
Lepiej użyć collect(Collectors.toList()) — jest to bezpieczniejsze i czytelniejsze.
Błąd nr 3: Próba ponownego użycia strumienia
Strumienia można użyć tylko raz. Po operacji terminalnej (np. collect, forEach) próba dalszej pracy z tym samym Stream doprowadzi do IllegalStateException.
Błąd nr 4: Naruszenie zasady „bez efektów ubocznych”
Operacje pośrednie powinny być „czyste” (bez zmiany zmiennych zewnętrznych). Nie należy wewnątrz map lub filter modyfikować niczego poza strumieniem.
Błąd nr 5: Brak uwzględnienia kolejności w Set i Map
Jeśli kolejność elementów jest ważna, używajcie odpowiednich kolekcji — na przykład LinkedHashSet, TreeMap — i wskazujcie właściwy kolektor.
GO TO FULL VERSION