mainejug.org

Praktyczna wiedza na co dzień

mainejug.org

Praktyczna wiedza na co dzień

Java

Streamy w Javie: praktyczne przykłady przetwarzania kolekcji

Stream API, wprowadzone w Javie 8, zmieniło sposób, w jaki wielu programistów przetwarza kolekcje. Zamiast pętli z licznikami i tymczasowymi listami można opisać, co chcemy uzyskać, w kilku czytelnych krokach. Strumienie nie są jednak rozwiązaniem na wszystko i mogą pogorszyć kod, gdy stosuje się je bezmyślnie. Ten wpis pokazuje kilka praktycznych wzorców i zasad ich używania.

Budowa potoku operacji

Każde przetwarzanie strumieniowe składa się z trzech części: źródła (np. kolekcji), zero lub więcej operacji pośrednich (jak filter czy map) oraz operacji końcowej (np. collect, count, forEach). Operacje pośrednie są leniwe, czyli nie wykonują się, dopóki nie zostanie wywołana operacja końcowa. Strumień można też zużyć tylko raz.

Załóżmy, że mamy listę zamówień i chcemy znać numery tych, które mają wartość powyżej 100 zł, w kolejności alfabetycznej:

List<String> numery = zamowienia.stream()
        .filter(z -> z.getWartosc().compareTo(new BigDecimal("100")) > 0)
        .map(Zamowienie::getNumer)
        .sorted()
        .collect(Collectors.toList());

Kod czyta się niemal jak opis: weź zamówienia, odfiltruj, przekształć na numery, posortuj i zbierz do listy. Od Javy 16 można skrócić końcówkę do .toList(), które zwraca listę niemodyfikowalną.

Grupowanie i zliczanie

Jednym z najbardziej użytecznych kolektorów jest groupingBy. Pozwala on w jednej instrukcji pogrupować elementy według wybranej cechy:

Map<Status, List<Zamowienie>> wgStatusu = zamowienia.stream()
        .collect(Collectors.groupingBy(Zamowienie::getStatus));

Map<Status, Long> liczby = zamowienia.stream()
        .collect(Collectors.groupingBy(Zamowienie::getStatus, Collectors.counting()));

Podobnie działają summingInt, averagingDouble czy mapping, które pozwalają dalej przetwarzać wartości w grupach. Do rozdzielenia elementów na dwie grupy według warunku służy partitioningBy, zwracający mapę z kluczami true i false.

Sumowanie i wyszukiwanie

Do operacji liczbowych wygodne są strumienie typów prostych: IntStream, LongStream i DoubleStream. Unikają opakowywania liczb w obiekty i mają gotowe metody takie jak sum(), average() czy max():

int lacznaLiczbaSztuk = pozycje.stream()
        .mapToInt(Pozycja::getIlosc)
        .sum();

Optional<Zamowienie> najdrozsze = zamowienia.stream()
        .max(Comparator.comparing(Zamowienie::getWartosc));

Metody anyMatch, allMatch i noneMatch sprawdzają warunki dla całej kolekcji, a findFirst zwraca pierwszy element spełniający kryteria. Wiele z nich przerywa przetwarzanie, gdy tylko znana jest odpowiedź, co oszczędza pracę.

Kiedy strumień pogarsza kod

Pomimo zalet nie każde zadanie nadaje się do strumieni. Zwróć uwagę na kilka sytuacji:

  • Gdy w pętli trzeba modyfikować zewnętrzne zmienne, zwykła pętla for jest często prostsza i bezpieczniejsza.
  • Bardzo długie potoki z kilkunastoma operacjami stają się nieczytelne; podziel je na mniejsze metody z nazwami opisującymi cel.
  • Wyrażenia lambda z wieloma liniami lepiej przenieść do osobnych metod i użyć referencji do metod.
  • Operacja forEach z efektami ubocznymi, takimi jak dopisywanie do zewnętrznej listy, to sygnał, że warto użyć collect.

Obsługa wyjątków kontrolowanych wewnątrz lambd bywa niewygodna. W takich przypadkach często lepiej zostać przy klasycznej pętli albo opakować wywołanie w osobną metodę.

Strumienie równoległe: ostrożnie

Wywołanie parallelStream() lub .parallel() może przyspieszyć obliczenia na dużych zbiorach, ale nie jest darmowe. Równoległość ma sens przy dużej liczbie elementów, kosztownych obliczeniach i operacjach bez efektów ubocznych oraz bez współdzielonego stanu. Przy małych kolekcjach narzut zarządzania wątkami potrafi sprawić, że program działa wolniej. Zanim zdecydujesz się na równoległość, wykonaj pomiar na realistycznych danych, najlepiej narzędziem do mikrobenchmarków, a nie na oko.

Dobre nawyki na koniec

  1. Nadawaj zmiennym i metodom nazwy opisujące sens, a nie technikę.
  2. Korzystaj z referencji do metod, gdy lambda tylko wywołuje jedną metodę.
  3. Unikaj zwracania null z lambd; rozważ Optional lub filtr.
  4. Pisz testy dla złożonych transformacji, korzystając z małych, czytelnych danych.
  5. Nie zmieniaj źródłowej kolekcji w trakcie przetwarzania strumienia.

Strumienie najlepiej ćwiczyć na własnym kodzie. Znajdź w projekcie pętlę, która filtruje i przekształca elementy, i spróbuj ją przepisać. Jeśli nowa wersja jest krótsza i czytelniejsza, zostaw ją; jeśli nie, pętla nadal jest pełnoprawnym narzędziem.

Jeśli dopiero zaczynasz, przejrzyj dokumentację interfejsu Collectors w swojej wersji JDK. Znajdziesz tam wiele gotowych kolektorów, takich jak joining do łączenia napisów, toMap do budowania map czy teeing od Javy 12, które pozwalają jednym przejściem policzyć dwa różne wyniki. Znajomość tych narzędzi często eliminuje potrzebę pisania własnych pętli pomocniczych i skraca kod bez utraty czytelności.

https://www.news-impuls.de/terrassenueberdachung-selber-bauen-schritt-fuer-schritt-anleitung/ | https://seahome.pl/pakiety-medyczne-dla-seniora-bezpieczenstwo-zdrowotne-na-emeryturze/ | https://medyczne-pakiety.com/domowy-internet-swiatlowodowy-bez-tajemnic-wybierz-madrze/