Przejdź do treści

Stream API w praktyce: filtrowanie, grupowanie i podsumowania

  • Kolekcje i strumienie
  • poziom: średni
  • aktualizacja: 15 sierpnia 2026

Stream API zastępuje pętle z ręcznym budowaniem list i map — zamiast pisać, jak przejść po kolekcji, opisujesz, co chcesz dostać. Największa pułapka to traktowanie strumienia jak kolekcji: strumień jest jednorazowy (po collect() nie użyjesz go ponownie), a bez operacji kończącej, takiej jak toList() czy forEach(), żaden filter() w ogóle się nie wykona. Ten skrypt pokazuje wszystkie najczęściej potrzebne wzorce na jednym, realistycznym zbiorze danych.

JAVA StrumienieWPraktyce.java 130 linii Pobierz .java
import java.util.Comparator;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;

/**
 * Stream API w praktyce: filtrowanie, grupowanie i podsumowania — skryptyJAVA.pl
 * Zapisz jako: StrumienieWPraktyce.java
 */
public class StrumienieWPraktyce {

    /** Pracownik — rekord z danymi wejściowymi dla wszystkich przykładów. */
    public record Pracownik(String imie, String nazwisko, String dzial, double pensja, int rokZatrudnienia) {

        /** Zwraca imię i nazwisko sklejone w jeden napis. */
        public String pelneImie() {
            return imie + " " + nazwisko;
        }
    }

    /** Zwraca przykładowy zbiór danych — podmień go na własną listę albo wynik zapytania do bazy. */
    public static List<Pracownik> przykladoweDane() {
        return List.of(
                new Pracownik("Anna", "Kowalska", "IT", 14200, 2017),
                new Pracownik("Piotr", "Nowak", "IT", 11800, 2020),
                new Pracownik("Marta", "Wiśniewska", "IT", 9600, 2022),
                new Pracownik("Tomasz", "Lewandowski", "Sprzedaż", 8900, 2015),
                new Pracownik("Ewa", "Dąbrowska", "Sprzedaż", 10400, 2019),
                new Pracownik("Michał", "Zieliński", "Sprzedaż", 7300, 2023),
                new Pracownik("Karolina", "Wójcik", "Marketing", 9900, 2018),
                new Pracownik("Rafał", "Szymański", "Marketing", 6800, 2021),
                new Pracownik("Beata", "Kaczmarek", "Kadry", 8200, 2016));
    }

    /** Formatuje kwotę do postaci „12 500,00 zł" niezależnie od ustawień systemu. */
    public static String kwota(double wartosc) {
        return String.format(Locale.ROOT, "%,.2f zł", wartosc).replace(',', ' ').replace('.', ',');
    }

    /** Wypisuje nagłówek sekcji, żeby wynik w konsoli był czytelny. */
    public static void sekcja(String tytul) {
        System.out.println();
        System.out.println("── " + tytul + " " + "─".repeat(Math.max(3, 72 - tytul.length())));
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) {
        List<Pracownik> pracownicy = przykladoweDane();
        double prog = 10000;

        sekcja("1. filter + map + toList()");
        List<String> dobrzeZarabiajacy = pracownicy.stream()
                .filter(p -> p.pensja() >= prog)
                .map(Pracownik::pelneImie)
                .toList();                                  // Java 16+; wcześniej: collect(Collectors.toList())
        System.out.println("Pensja od " + kwota(prog) + ": " + dobrzeZarabiajacy);

        sekcja("2. sorted + Comparator.comparing / thenComparing / reversed");
        pracownicy.stream()
                .sorted(Comparator.comparing(Pracownik::dzial)
                        .thenComparing(Comparator.comparingDouble(Pracownik::pensja).reversed()))
                .forEach(p -> System.out.printf("%-10s %-22s %s%n", p.dzial(), p.pelneImie(), kwota(p.pensja())));

        sekcja("3. groupingBy + mapping — dział na listę nazwisk");
        Map<String, List<String>> wedlugDzialu = pracownicy.stream()
                .collect(Collectors.groupingBy(Pracownik::dzial, TreeMap::new,
                        Collectors.mapping(Pracownik::nazwisko, Collectors.toList())));
        wedlugDzialu.forEach((dzial, osoby) -> System.out.println(dzial + ": " + osoby));

        sekcja("4. groupingBy + counting / summingDouble / averagingDouble");
        Map<String, Long> liczebnosc = pracownicy.stream()
                .collect(Collectors.groupingBy(Pracownik::dzial, TreeMap::new, Collectors.counting()));
        Map<String, Double> sumaPensji = pracownicy.stream()
                .collect(Collectors.groupingBy(Pracownik::dzial, TreeMap::new,
                        Collectors.summingDouble(Pracownik::pensja)));
        Map<String, Double> sredniaPensja = pracownicy.stream()
                .collect(Collectors.groupingBy(Pracownik::dzial, TreeMap::new,
                        Collectors.averagingDouble(Pracownik::pensja)));
        liczebnosc.forEach((dzial, ile) -> System.out.printf("%-10s osób: %d, suma: %s, średnia: %s%n",
                dzial, ile, kwota(sumaPensji.get(dzial)), kwota(sredniaPensja.get(dzial))));

        sekcja("5. Podsumowania — sum, min, max, average, summarizingDouble");
        double sumaWszystkich = pracownicy.stream().mapToDouble(Pracownik::pensja).sum();
        System.out.println("Suma pensji:  " + kwota(sumaWszystkich));
        DoubleSummaryStatistics staty = pracownicy.stream()
                .collect(Collectors.summarizingDouble(Pracownik::pensja));
        System.out.printf("Liczba: %d, min: %s, max: %s, średnia: %s%n",
                staty.getCount(), kwota(staty.getMin()), kwota(staty.getMax()), kwota(staty.getAverage()));

        sekcja("6. Optional — max() i findFirst() bez ryzyka NullPointerException");
        Optional<Pracownik> najlepiejPlatny = pracownicy.stream()
                .max(Comparator.comparingDouble(Pracownik::pensja));
        System.out.println("Najwyższa pensja: " + najlepiejPlatny.map(Pracownik::pelneImie).orElse("brak danych"));
        pracownicy.stream()
                .filter(p -> p.dzial().equals("Kadry"))
                .findFirst()
                .ifPresent(p -> System.out.println("Pierwszy z kadr: " + p.pelneImie()));
        String brakujacy = pracownicy.stream()
                .filter(p -> p.rokZatrudnienia() < 2010)
                .findFirst()
                .map(Pracownik::pelneImie)
                .orElse("nikt nie pracuje tu od 2010 roku");
        System.out.println("Zatrudniony przed 2010: " + brakujacy);

        sekcja("7. joining — sklejanie tekstu przecinkami");
        String weterani = pracownicy.stream()
                .filter(p -> p.rokZatrudnienia() <= 2018)
                .sorted(Comparator.comparingInt(Pracownik::rokZatrudnienia))
                .map(Pracownik::pelneImie)
                .collect(Collectors.joining(", "));
        System.out.println("Zatrudnieni do 2018: " + weterani);

        sekcja("8. partitioningBy — podział na dwie grupy");
        Map<Boolean, List<String>> podzial = pracownicy.stream()
                .collect(Collectors.partitioningBy(p -> p.pensja() >= prog,
                        Collectors.mapping(Pracownik::pelneImie, Collectors.toList())));
        System.out.println("Powyżej progu: " + podzial.get(true));
        System.out.println("Poniżej progu: " + podzial.get(false));

        sekcja("9. anyMatch / allMatch / noneMatch — szybkie pytania tak-nie");
        System.out.println("Ktoś ponad 14 000?  " + pracownicy.stream().anyMatch(p -> p.pensja() > 14000));
        System.out.println("Wszyscy ponad 5 000? " + pracownicy.stream().allMatch(p -> p.pensja() > 5000));
        System.out.println("Nikt z działu Prawny? "
                + pracownicy.stream().noneMatch(p -> p.dzial().equals("Prawny")));
    }
}

Jak użyć tego skryptu

  1. Skopiuj kod i zapisz go w pliku StrumienieWPraktyce.java — nazwa pliku i klasy publicznej muszą być takie same — a potem uruchom w terminalu poleceniem java StrumienieWPraktyce.java. Zobaczysz dziewięć ponumerowanych sekcji — każda wypisuje wynik innej operacji na tych samych danych.
  2. Podmień metodę przykladoweDane() na własne źródło: listę wczytaną z pliku, wynik zapytania do bazy albo dane z API. Reszta kodu zadziała bez zmian, o ile zachowasz wszystkie pola rekordu Pracownik — sekcje 6 i 7 sięgają też po rokZatrudnienia, a 1 i 3 po imie i nazwisko.
  3. Do własnego projektu przenoś pojedyncze sekcje — to samodzielne fragmenty. Najczęściej przydają się: filter().map().toList(), Collectors.groupingBy(...) i Collectors.summarizingDouble(...).
  4. Chcesz posortować inaczej? Zmień komparator w sekcji 2 — Comparator.comparing(Pracownik::nazwisko) uporządkuje ludzi po nazwisku, a .reversed() doklejone na końcu całego łańcucha odwróci wszystkie kryteria naraz, nie tylko ostatnie. Nazwiska pokazywane użytkownikowi sortuj Collatorem dla polskiego — samo comparing porównuje kody Unicode, więc „Ćma" wyląduje za „Zebrą".

Warto wiedzieć

  • Strumień jest jednorazowy. Po operacji kończącej (toList(), collect(), forEach()) nie użyjesz go ponownie — dostaniesz IllegalStateException. Dlatego w sekcji 4 każde grupowanie zaczyna się od nowego pracownicy.stream().
  • Zwykłe groupingBy zwraca HashMap, a w niej kolejność kluczy jest nieokreślona — przy kolejnym uruchomieniu działy mogą wypaść w innej kolejności. Dlatego w kodzie przekazujemy TreeMap::new: wynik grupowania jest wtedy zawsze posortowany po kluczu.
  • Lista zwrócona przez toList() jest niemodyfikowalna — próba add() rzuci UnsupportedOperationException. Jeśli potrzebujesz listy do zmiany, użyj collect(Collectors.toList()) albo collect(Collectors.toCollection(ArrayList::new)).
  • Do kwot pieniężnych w prawdziwej aplikacji używaj BigDecimal, nie double — tu double jest tylko po to, żeby pokazać summingDouble i averagingDouble. Przy dodawaniu setek kwot błędy zaokrągleń double potrafią urosnąć do groszy, których nie da się wytłumaczyć księgowej.
#stream api #kolekcje #grupowanie #sortowanie #comparator #lambda

Podobne skrypty