CodeGym /Kursy /JAVA 25 SELF /Styl funkcyjny z Stream API

Styl funkcyjny z Stream API

JAVA 25 SELF
Poziom 49, Lekcja 3
Dostępny

1. Imperatywny vs funkcyjny styl

Zacznijmy od prostego pytania: po co w ogóle ten cały „styl funkcyjny”? Czym jest lepszy od zwykłego, dobrze znanego podejścia z pętlami? I co w ogóle oznacza „styl funkcyjny” w Javie?

Styl imperatywny

Styl imperatywny – to gdy mówisz komputerowi jak coś zrobić, krok po kroku. Na przykład, jeśli z listy napisów chcesz uzyskać listę ich długości, zostawić tylko nieparzyste długości i posortować je malejąco, napiszesz mniej więcej tak:

List<String> words = Arrays.asList("kot", "słoń", "nosorożec", "tygrys", "mysz");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [9, 3]

Tutaj jawnie tworzymy listę pośrednią, ręcznie dodajemy elementy, sortujemy – wszystko krok po kroku.

Styl funkcyjny

Styl funkcyjny – to gdy opisujesz co chcesz uzyskać, a nie jak to zrobić. W Javie jest to zrealizowane przez Stream API:

List<String> words = Arrays.asList("kot", "słoń", "nosorożec", "tygrys", "mysz");

List<Integer> result = words.stream()
    .map(String::length)
    .filter(len -> len % 2 != 0)
    .sorted(Comparator.reverseOrder())
    .toList();

System.out.println(result); // [9, 3]

Tutaj jakby budujemy „taśmę” przetwarzania danych: najpierw zamieniamy słowa na ich długości (map), potem filtrujemy nieparzyste (filter), potem sortujemy (sorted). Wszystko to – w jednym łańcuchu, bez jawnych kolekcji pośrednich i pętli.

Porównanie: iteracyjny vs funkcyjny styl

W tradycyjnym podejściu imperatywnym piszemy pętlę, w której krok po kroku tłumaczymy komputerowi, co robić: przejść po każdym elemencie, sprawdzić warunek, dodać do nowej listy lub wypisać na ekran. Kod działa, ale zajmuje więcej linii i im bardziej złożone zadanie, tym trudniej go czytać i utrzymywać.

Styl funkcyjny pozwala opisywać nie sam proces, lecz to, co chcemy uzyskać. Zamiast długiej pętli budujemy łańcuch operacji: odfiltrować, przekształcić, zebrać wynik. To krótsze, bardziej przejrzyste i zmniejsza ryzyko błędów, bo jest mniej „ręcznej roboty” z mutowalnymi kolekcjami.

Jest też druga strona medalu. Dla początkujących taki łańcuch może wyglądać na zagmatwany: kilka lambd pod rząd czyta się trudniej niż prostą pętlę. Dlatego styl funkcyjny wygrywa pod względem zwięzłości i ekspresyjności, ale wymaga przyzwyczajenia i obycia.

2. Podstawowe operacje Stream API

Stream API to nie po prostu „nowy rodzaj pętli”, lecz cały zestaw narzędzi do przetwarzania kolekcji w stylu funkcyjnym. Przyjrzyjmy się podstawowym operacjom.

Jak uzyskać Stream?

List<String> list = List.of("a", "bb", "ccc");
Stream<String> stream = list.stream();

Operacje pośrednie

  • map – przekształca elementy strumienia
  • filter – filtruje elementy według warunku
  • flatMap – zamienia każdy element na strumień i „spłaszcza” je
  • sorted – sortowanie
  • distinct – usuwa duplikaty
  • limit / skip – ogranicza/pomija elementy

Operacje terminalne

  • forEach – wykonuje działanie dla każdego elementu
  • collect – zbiera wynik do kolekcji
  • reduce – sprowadza strumień do jednej wartości (np. sumy)
  • count – liczy elementy
  • anyMatch, allMatch, noneMatch – sprawdzanie warunków

Przykład: łańcuch przetwarzania

List<String> names = List.of("Anna", "Borys", "Wika", "Gleb", "Dasza");

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

System.out.println(filtered); // [ANNA, BORYS, DASZA, GLEB, WIKA]

Wizualny schemat „taśmy”:

[Anna, Borys, Wika, Gleb, Dasza]
   | filter (length>3)
[Anna, Borys, Wika, Gleb, Dasza]
   | map (toUpperCase)
[ANNA, BORYS, WIKA, GLEB, DASZA]
   | sorted
[ANNA, BORYS, DASZA, GLEB, WIKA]
   | toList

Każda operacja nie zmienia kolekcji źródłowej – tworzony jest nowy strumień.

3. Przetwarzanie strumieniowe i leniwość

Operacje pośrednie i terminalne

W Streamach są dwa typy operacji. Pierwsze – pośrednie, takie jak map, filter czy sorted. Zwracają nowy strumień i jakby obiecują coś zrobić, ale tak naprawdę jeszcze nic się nie wykonuje. Drugie – terminalne, na przykład forEach, collect czy reduce. To one faktycznie uruchamiają całe przetwarzanie. Kluczowe jest to, że dopóki nie wywołasz operacji terminalnej, strumień pozostaje „leniwy” – obliczenia się nie zaczynają.

Przykład:

Stream<String> stream = List.of("a", "bb", "ccc").stream()
    .map(s -> {
        System.out.println("map: " + s);
        return s.toUpperCase();
    });

System.out.println("Przed forEach");
stream.forEach(System.out::println);

Wynik:

Przed forEach
map: a
A
map: bb
BB
map: ccc
CCC

Widać, że map nie wykonuje się, dopóki nie zacznie się forEach.

Dlaczego to fajne?

Takie podejście pozwala bez zbędnych kosztów budować dowolnie długie łańcuchy przekształceń. Strumień zacznie działać dopiero wtedy, gdy będzie to naprawdę potrzebne. Dzięki temu można bez problemu przetwarzać ogromne zbiory danych, a nawet nieskończone sekwencje. Leniwość pomaga też oszczędzać pamięć i wykonywać obliczenia wydajniej.

4. Praktyka: zadanie „Napisy → długości → nieparzyste → malejąco”

Przejdźmy krok po kroku przez zadanie: „Z listy napisów uzyskać listę długości napisów, zostawić tylko nieparzyste długości, posortować malejąco.”

Rozwiązanie imperatywne

List<String> words = Arrays.asList("kot", "słoń", "nosorożec", "tygrys", "mysz");
List<Integer> lengths = new ArrayList<>();

for (String word : words) {
    int len = word.length();
    if (len % 2 != 0) {
        lengths.add(len);
    }
}
lengths.sort(Comparator.reverseOrder());
System.out.println(lengths); // [9, 3]

Rozwiązanie funkcyjne ze Stream API

List<String> words = Arrays.asList("kot", "słoń", "nosorożec", "tygrys", "mysz");

List<Integer> result = words.stream()
    .map(String::length) // zamieniamy napisy na ich długości
    .filter(len -> len % 2 != 0) // zostawiamy tylko nieparzyste długości
    .sorted(Comparator.reverseOrder()) // sortujemy malejąco
    .toList(); // zbieramy do List (Java 16+)

System.out.println(result); // [9, 3]

Wyjaśnienia:

  • map(String::length) – dla każdego napisu pobieramy jego długość.
  • filter(len -> len % 2 != 0) – zostawiamy tylko nieparzyste długości.
  • sorted(Comparator.reverseOrder()) – sortujemy malejąco.
  • toList() – zbieramy strumień do nowej listy.
Analogią

To tak, jakbyś budował „taśmę” w fabryce: na każdym etapie elementy są przetwarzane na nowo, a dopiero na końcu wszystko trafia do pudełka.

5. Jeszcze przykłady: map, filter, forEach, collect

Przykład 1: Filtrowanie i wypisanie

List<String> names = List.of("Anna", "Borys", "Wika", "Gleb", "Dasza");

names.stream()
    .filter(name -> name.contains("a"))
    .forEach(System.out::println);
// Wypisze: Anna, Wika, Dasza

Przykład 2: Przekształcenie i zebranie do Set

Set<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toSet());

System.out.println(upperNames); // [ANNA, BORYS, WIKA, GLEB, DASZA]

Przykład 3: Suma długości wszystkich napisów

int totalLength = names.stream()
    .mapToInt(String::length)
    .sum();

System.out.println("Łączna długość: " + totalLength);

Przykład 4: Użycie Predicate i Function

Predicate<String> longName = name -> name.length() > 4;
Function<String, String> greet = name -> "Cześć, " + name + "!";

names.stream()
    .filter(longName)
    .map(greet)
    .forEach(System.out::println);
// Cześć, Borys!
// Cześć, Dasza!

6. Cechy stylu funkcyjnego ze Stream API

Brak mutowalnego stanu

Styl Stream API promuje „czyste” funkcje – bez efektów ubocznych. To znaczy, że lepiej nie zmieniać zmiennych zewnętrznych wewnątrz lambd.

Źle:

List<String> result = new ArrayList<>();
names.stream()
    .filter(name -> name.startsWith("A"))
    .forEach(result::add); // efekt uboczny!

Lepiej:

List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList();

Kompozycja operacji

Można budować bardzo długie łańcuchy, łącząc map, filter, sorted i inne metody. Najważniejsze – nie przesadzać: jeśli łańcuch stał się dłuższy niż ekran, być może warto podzielić go na części.

Leniwe obliczenia

Stream API nie robi nic, dopóki nie dojdzie do operacji terminalnej. To pozwala oszczędzać zasoby i budować wydajne potoki przetwarzania danych.

Niemodyfikowalność kolekcji źródłowych

Strumień nie zmienia kolekcji źródłowej! Wszystkie przekształcenia zwracają nowy strumień/kolekcję.

7. Kiedy używać Stream API

Stream API świetnie się sprawdza, gdy:

  • Trzeba szybko przetworzyć kolekcję (filtrowanie, przekształcanie, sortowanie).
  • Potrzebny jest zwięzły, czytelny kod.
  • Nie chcesz ręcznie tworzyć kolekcji pośrednich.
  • Chcesz łatwo dodać równoległość (przez parallelStream()).

Styl imperatywny bywa lepszy, jeśli:

  • Potrzebna jest złożona logika z wieloma zagnieżdżonymi pętlami i warunkami.
  • Ważna jest maksymalna wydajność w miejscach krytycznych (Stream API bywa nieco wolniejsze).
  • Trzeba pracować ze stanem mutowalnym (np. aktualizować elementy „w miejscu”).

8. Typowe błędy przy pracy ze Stream API

Błąd nr 1: Używanie forEach do zbierania kolekcji. Wielu początkujących używa forEach do dodawania elementów do nowej kolekcji. To nie jest styl funkcyjny! Zamiast tego używaj collect lub toList().

Błąd nr 2: Przedwczesna optymalizacja. Nie próbuj od razu używać parallelStream() – równoległość ma sens tylko dla naprawdę dużych kolekcji i zadań CPU-intensywnych.

Błąd nr 3: Mieszanie Stream API i mutowalnych kolekcji. Stream API zakłada pracę z danymi niemutowalnymi. Nie należy wewnątrz lambd zmieniać elementów kolekcji.

Błąd nr 4: „Utrata” wyniku. Jeśli zapomnisz wywołać operację terminalną – nic się nie wydarzy.

Błąd nr 5: Zbyt złożone lambdy. Jeśli wyrażenie lambda stało się dłuższe niż jedna–dwie linie – wynieś je do osobnej metody z czytelną nazwą.

1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 3
Niedostępne
Ocena "Magicznego ekwipunku" w grze tekstowej 🧙‍♂️
Ocena "Magicznego ekwipunku" w grze tekstowej 🧙‍♂️
1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 3
Niedostępne
Analiza Miejscowości dla Tajemniczej Ekspedycji 🗺️
Analiza Miejscowości dla Tajemniczej Ekspedycji 🗺️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION