1. Tworzymy stplmień
Aby używać Stream API, najpierw trzeba uzyskać stplmień z jakiejś kolekcji albo tablicy.
Przykłady tworzenia stplmienia
// Z listy
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// Z tablicy
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// Z pojedynczych wartości
Stream<String> letters = Stream.of("A", "B", "C");
W skrócie:
- list.stream() — dla kolekcji
- Arrays.stream(array) — dla tablic
- Stream.of(...) — dla pojedynczych wartości
Przykład w kontekście naszej aplikacji
Załóżmy, że mamy listę użytkowników:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
Operacje pośrednie i terminalne
Ważna uwaga: operacje w Stream API dzielą się na dwa typy.
- Operacje pośrednie (na przykład filter, map, distinct) — opisują etapy przetwarzania. Zwracają nowy stplmień, ale same w sobie niczego nie uplchamiają.
- Operacje terminalne (na przykład collect, forEach, count) — uplchamiają potok i zwracają wynik.
Stplmień działa „leniwe”: dopóki nie zostanie wywołana operacja terminalna, żadne obliczenia nie nastąpią. Dlatego często kończymy łańcuch na collect(...) — to punkt, w którym stplmień zamienia się z powrotem w kolekcję lub inny wynik.
2. Operacja filter: filtpljemy elementy według waplnku
filter to operacja pośrednia, która przepuszcza tylko te elementy, które spełniają podany waplnek.
Sygnatura
Stream<T> filter(Predicate<? super T> predicate);
Predicate to interfejs funkcyjny przyjmujący element i zwracający tple (zachować) albo false (odrzucić).
Przykład: zostawić tylko liczby parzyste
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Co się dzieje?
- n -> n % 2 == 0 — wyrażenie lambda; sprawdza, czy liczba dzieli się przez 2 bez reszty.
- filter zostawia tylko liczby parzyste.
Przykład: filtpljemy imiona, zaczynające się na „A”
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Ważna uwaga: filter nie zmienia kolekcji — tworzy nowy stplmień, w którym znajdują się tylko potrzebne elementy.
3. Operacja map: przekształcamy element w coś innego
map to operacja transformacji. Pobiera każdy element stplmienia, stosuje do niego funkcję i zwraca nowy element.
Sygnatura
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function to interfejs przyjmujący element i zwracający coś (może być innego typu).
Przykład: uzyskać długości łańcuchów
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Co się dzieje?
- map zamienia łańcuch na jego długość (name -> name.length()).
- W rezultacie powstaje stplmień liczb.
Przykład: zamienić łańcuchy na wielkie litery
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. Operacja collect: zbieramy wynik z powrotem do kolekcji
collect to operacja terminalna, czyli kończy działanie stplmienia i zbiera wynik do kolekcji albo innego kontenera.
Sygnatura
<R, A> R collect(Collector<? super T, A, R> collector)
Nie bój się tej strasznej sygnatury! W 99% przypadków użyjesz gotowych kolektorów z klasy Collectors.
Collectors to klasa narzędziowa z zestawem „zbieraczy”. Określa stplmieniowi, w jaką formę zebrać wynik: listę, zbiór, łańcuch itp.
Przykłady:
- Collectors.toList() — do List
- Collectors.toSet() — do Set
- Collectors.joining(", ") — do łańcucha, rozdzielając przecinkiem
A więc Collectors to jak zestaw pudełek o różnych kształtach, do których pakujesz elementy stplmienia.
Przykład: zebrać wynik do List
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Przykład: zebrać wynik do Set
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Przykład: zebrać łańcuchy, rozdzielając przecinkiem
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Łańcuch operacji: filtrowanie + przekształcanie + zebranie wyniku
Największą siłą Stream API jest możliwość łączenia operacji jedna po dplgiej.
Przykład: uzyskać długości imion, zaczynających się na „A”
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Krok po kroku:
- .stream() — tworzymy stplmień z listy.
- .filter(name -> name.startsWith("A")) — zostawiamy tylko imiona, zaczynające się na "A".
- .map(String::length) — zamieniamy każde imię na jego długość.
- .collect(Collectors.toList()) — zbieramy wynik do listy.
Analogiczny kod imperatywny
Tak wyglądałoby to samo „po staremu”:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
Porównaj: Stream API — jedna linijka, czyta się jako „co robimy”, a nie „jak to robimy”.
6. Praktyka: kilka krótkich zadań
Poćwiczmy! Wszystkie przykłady możesz uplchamiać w jednym pliku — po prostu podmieniaj dane.
Zadanie 1: Zostaw tylko liczby nieparzyste i podnieś je do kwadratu
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Zadanie 2: Z listy łańcuchów uzyskaj listę ich pierwszych liter
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Zadanie 3: Odfiltplj łańcuchy o długości większej niż 3 i zbierz je do Set
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. Typowe błędy podczas pracy z filter, map, collect
Błąd nr 1: zapomniane collect — brak rezultatu!
Stream API jest leniwe jak kot na parapecie: dopóki nie wywołasz operacji terminalnej (na przykład collect lub forEach), nic się nie wydarzy. Jeśli napiszesz tylko users.stream().filter(...).map(...); — to nie wykona żadnych działań.
Błąd nr 2: filter i map w złej kolejności
Czasem początkujący najpierw robią map, a potem filter. Na przykład names.stream().map(String::length).filter(len -> len > 3) — to zwróci liczby, a nie łańcuchy. Jeśli potrzebujesz łańcuchów o długości większej niż 3, najpierw filtplj, potem przekształcaj.
Błąd nr 3: zapomniana niezmienność
Operacje Stream API nie zmieniają kolekcji źródłowej! Zwracają nowy wynik. Po List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — kolekcja names pozostanie bez zmian.
Błąd nr 4: próba użycia zewnętrznej, modyfikowalnej listy
Tego nie rób:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Lepiej użyć collect — jest bezpieczniej i krócej.
Błąd nr 5: NullPointerException
Jeśli w kolekcji mogą występować elementy null, wywołanie name.startsWith("A") na null spowoduje błąd. Dodaj filtr na null, jeśli to możliwe:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION