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.
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
- Skopiuj kod i zapisz go w pliku
StrumienieWPraktyce.java— nazwa pliku i klasy publicznej muszą być takie same — a potem uruchom w terminalu poleceniemjava StrumienieWPraktyce.java. Zobaczysz dziewięć ponumerowanych sekcji — każda wypisuje wynik innej operacji na tych samych danych. - 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 rekorduPracownik— sekcje 6 i 7 sięgają też porokZatrudnienia, a 1 i 3 poimieinazwisko. - Do własnego projektu przenoś pojedyncze sekcje — to samodzielne fragmenty. Najczęściej przydają się:
filter().map().toList(),Collectors.groupingBy(...)iCollectors.summarizingDouble(...). - 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 — samocomparingporó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 — dostanieszIllegalStateException. Dlatego w sekcji 4 każde grupowanie zaczyna się od nowegopracownicy.stream(). - Zwykłe
groupingByzwracaHashMap, a w niej kolejność kluczy jest nieokreślona — przy kolejnym uruchomieniu działy mogą wypaść w innej kolejności. Dlatego w kodzie przekazujemyTreeMap::new: wynik grupowania jest wtedy zawsze posortowany po kluczu. - Lista zwrócona przez
toList()jest niemodyfikowalna — próbaadd()rzuciUnsupportedOperationException. Jeśli potrzebujesz listy do zmiany, użyjcollect(Collectors.toList())albocollect(Collectors.toCollection(ArrayList::new)). - Do kwot pieniężnych w prawdziwej aplikacji używaj
BigDecimal, niedouble— tudoublejest tylko po to, żeby pokazaćsummingDoubleiaveragingDouble. Przy dodawaniu setek kwot błędy zaokrągleńdoublepotrafią urosnąć do groszy, których nie da się wytłumaczyć księgowej.
Podobne skrypty
Najczęstsze operacje na List, Map i Set
Operacje, które robisz na kolekcjach codziennie: dodawanie i usuwanie elementów listy, kasowanie duplikatów, część wspólna i różnica zbiorów oraz zliczanie i grupowanie danych w mapie.
początkujący Zobacz kod → Kolekcje i strumienieSortowanie obiektów przez Comparator i Collator
Sortowanie rekordów po wielu polach naraz: rosnąco i malejąco, z bezpieczną obsługą wartości null i polskim porządkiem liter — Comparator, thenComparing i Collator w praktyce.
średni Zobacz kod → Kolekcje i strumienieRecord, equals/hashCode i niemodyfikowalne kolekcje
Rekordy z walidacją w konstruktorze kompaktowym, automatyczne equals i hashCode w roli elementu zbioru i klucza mapy, obronna kopia List.copyOf oraz różnica między migawką a widokiem Collections.unmodifiableList.
średni Zobacz kod →