CodeGym /Kursy /JAVA 25 SELF /Przekształcanie kolekcji za pomocą Stream

Przekształcanie kolekcji za pomocą Stream

JAVA 25 SELF
Poziom 30 , Lekcja 4
Dostępny

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.

1
Ankieta/quiz
Podstawy Stream API, poziom 30, lekcja 4
Niedostępny
Podstawy Stream API
Podstawy Stream API
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION