Skip to content

Witaj w Świecie Jutra!

  • Technologie jutra
  • Sprzęt jutra
  • Aplikacje jutra
  • Programowanie
  • Księga Drogi
  • Renowacja
  • Różności
  • Archiwum
  • Autor
  • Home
  • Wszystko
  • Programowanie funkcyjne – Strumienie (2/2)
  • Programowanie
  • Wszystko

Programowanie funkcyjne – Strumienie (2/2)

Jakub Raczkowski 30 marca 2024

Mamy to. W końcu, po serii tekstów dorośliśmy aby zacząć korzystać z Java Stream API! Wiemy jakich interfejsów używać, a także jak za pomocą lambdy albo odniesienia stworzyć na cito porządaną metodę. Pozostało już zatem tylko jedno zadanie – zbudować swój pierwszy strumień!

Słowo „zbudować” zostało użyte nieprzypadkowo. Jeśli pamiętacie wzorzec Builder/Budowniczy to wiecie czego się spodziewać. Poszczególne metody strumienia nie zmieniają oryginalnego obiektu, tylko tworzą nowe w oparciu o poprzednika.

.stream()
.map()
.distinct()
.filter()
.sorted()
//i tak dalej

Generalnie, aby utworzyć strumień potrzebna nam będzie jakaś kolekcja (czyli obiekt klasy dziedziczącej po Collections, tj. Set, List, Stack, Queue, Map). Ale są od tego wyjątki.

Możemy stworzyć pusty strumień – do tego oczywiście nie potrzebujemy żadnego obiektu. Możemy użyć tabeli. Możemy nawet skorzystać z jakiegoś pliku, a znajdujące się w nim linie tekstu staną się automatycznie elementami naszego strumienia:

Path path = Paths.get("file.txt");
Stream<String> lines = Files.lines(path);

To samo można zrobić ze ścieżką do katalogu aby utworzyć strumień ze znajdujących się w nim plików – tak zbierałem pe-de-efy z CV w projekcie ITCandidateEvaluator:

Stream<Path> paths = 
Files.list(Paths.get(directoryWithResumesInPdf)))

Możliwości jest sporo, ale dla ułatwienia, w dzisiejszych przykładach będziemy używać jedynie tradycyjnych kolekcji.

Operacje na strumieniach

IntStream intStream = 
IntStream.of(1,3,3,7,8,10,100);
intStream.(?)

Strumień stworzony, wciskamy operator kropki aby podejrzeć dostępne metody… i co teraz?

Cóż, wszystko zależy od tego co chcemy osiągnąć. Jeśli nasz strumień wygląda jak w kodzie powyżej i zawiera same liczby rzeczywiste, a chcemy zmiennoprzecinkowe, to śmiało możemy je zmapować na typ 'double’. Innej metody użyjemy kiedy zależy nam aby wydobyć wszystkie liczby parzyste, a jeszcze innej aby dostać kolekcję pozbawioną jakichkolwiek duplikatów.

Przejdźmy sobie teraz przez najczęściej używane metody w javowych strumieniach:

map(Function f) & flatMap(Function f)

Dla wielu z was to będzie oczywista oczywistość, ale i tak zaznaczę, że mapa w świecie kodu to coś innego niż ten kawałek papieru, który zabieramy ze sobą na wycieczkę. Istotą mapy jest zbiór par klucz-wartość, z czego KLUCZ MUSI BYĆ UNIKALNY (choć wartości mogą się powtarzać). Dla przykładu:

<Pesel, Miasto>
<IdUżytkownika, Grupa>
<Potrawa, Smak>
<Piosenka, GatunekMuzyczny>

Stąd czasownik „zmapować” oznacza ni mniej ni więcej, tylko nadać danemu kluczowi jakąś wartość. To właśnie czyni metoda map() przyjmująca za parametr jakąś funkcję (interfejs Function). Może się ona przydać kiedy tworzymy strumień jakichś użytkowników, ale potrzebujemy np. tylko ich imienia. Wtedy robimy poniższy myk:

Stream<User> userStream = users.stream().map(User::getName)

Od tej pory dostajemy całkiem nowy strumień, ale zamiast obiektów User, mamy już tam same ciągi z imionami.

Ciekawą wersję mapowania dostarcza metoda flatMap(), gdyż potrafi ona „spłaszczyć kolekcję”. Wyobraźcie sobie, że ten obiekt User ma również zmienną typu List favouriteFoods. Jeśli spróbowalibyśmy tę listę zmapować jak poprzednio dostalibyśmy ją pogrupowaną wedle kolejnych użytkowników. Natomiast metoda flatMap() zwraca nam kolejny strumień zawierający tylko i wyłącznie elementy tych wszystkich list, bez żadnego grupowania:

filter(Predicate p)

Druga najważniejsza i najczęściej spotykana metoda strumieniowa. Bierzemy jakieś stwierdzenie (interfejs Predicate) i według niego filtrujemy elementy strumienia. Jeśli dany zawodnik wrzucony do naszego warunku zwraca 'true’ to idzie dalej, w przeciwnym wypadku zostaje na sitku. To może być np:

  • imię dłuższe od 5 znaków (user -> user.getName().length() > 5)
  • wiek ma być parzysty (user -> user.getAge() % 2 == 0)
  • wśród ulubionych potraw musi być pizza (user -> user.getFavouriteFoods().contains(„pizza”))
  • tylko kobieta (user -> user.getGender().equals(User.Gender.F))
  • tylko niezamężna (user -> !user.isMarried())

sorted(Comparator c)

Skoro zmapowaliśmy sobie kolekcję na pożądany typ, potem przepuściliśmy ją przez wszystkie niezbędne filtry, to teraz wypadałoby to jeszcze posortować.

Metoda sorted() przyjmuje obiekt typu Comparator, który definiuje na jakich zasadach mamy uporządkować elementy strumienia. Czy alfabetycznie? Czy na podstawie długości zapisu bitowego? Rosnąco, a może malejąco?

Możemy użyć wersji sorted() beż żadnych parametrów – to dobrze zadziała do typów prostych i Stringów. A jeśli zapiszemy to w poniższy sposób to odwrócimy kolejność:

.sorted(Comparator.reverseOrder())

Nic jednak nie stoi na przeszkodzie aby naszych Userów uporządkować wedle własnych reguł, np.

  • wedle wieku: (u1,u2) -> u1.getAge() – u2.getAge()
  • wedle długości imienia: (u1,u2) -> u1.getName().length() – u2.getName().length()
  • wedle imienia, alfabetycznie: (u1,u2) -> u1.getName().compareTo(u2.getName())

peek(Consumer c)

Peek() to raczej metoda pomocnicza – z angielskiego ’peek’ oznacza ’podejrzeć’ i to jest dokładnie to co ta metoda robi. Jeśli czasem coś nam nie będzie grało w strumieniu to pomiędzy pozostałymi metodami możemy walnąć sobie peek() i na przykład wypisać aktualne elementy w konsoli, albo zalogować do dziennika, albo skopiować do innej kolekcji. Cokolwiek co może zostać obsłużone Consumerem.

Peek() w żaden sposób nie modyfikuje stanu strumienia – służy tylko do podglądania.

distinct()

To metoda żywcem wyjęta z SQL’a – tam wygląda tak samo i robi dokładnie to samo czyli sprawia, że wypisane elementy się nie powtarzają. Jeśli zależy nam na unikalnej kolekcji to wstawiamy distinct() i gotowe. O ile oczywiście nie zapisujemy strumienia do Setu, bo tam i tak nie może być żadnych duplikatów i w/w metoda będzie zbyteczna.

limit(long n) & skip(long n)

Obie powyższe metody pobierają typ long jako swój parametr – limit(n) narzuca… no właśnie limit na ilość elementów strumienia. Natomiast skip(n) pomija pierwszych n elementów. Jeśli strumień ma 30 elementów i damy tam coś takiego:

.skip(10)
.limit(10)

To zostaną zwrócone elementy 11-20, razem 10 sztuk. Tylko pamiętajmy o właściwej kolejności, bo jak położymy to odwrotnie to nic nie dostaniemy.

Finiszery

Powyższe metody są jak najbardziej przydatne, ale same w sobie nie dają nam praktycznie nic. Co z tego, że zmapujemy jakieś obiekty na inne, przefiltrujemy i posortujemy, jeśli nie będzie nam dane zobaczyć rezultatów tych operacji?

Pewnym wyjściem jest oczywiście odpowiednie użycie metody forEach() aby np. dodać każdy element strumienia do już istniejącej kolekcji albo po prostu wyświetlić go w konsoli. Ale do ideału temu rozwiązaniu daleko i w zastosowaniach komercyjnych się nie sprawdzi.

Innym sposobem na zatrzymanie strumienia jest skorzystanie z jednej z funkcji kończących, dla elegancji nazwijmy je finiszerami. Takie fatality dosłownie zatrzymuje strumień, który od tej pory staje się zupełnie bezużyteczny – jakakolwiek próba reaktywacji wywali nam:

Exception in thread "main" 
java.lang.IllegalStateException: 
stream has already been operated upon 
or closed.

Popularne funkcje kończące to:

  • forEach(Consumer c) –> wykonuje zadaną operację na każdym elemencie strumienia po czym go zamyka;
  • toArray() –> tworzy tabelę (typu takiego jak elementy strumienia) i zatrzymuje strumień;
  • count() –> zwraca wartość typu prostego long, która jest liczbą pozostałych elementów strumienia (i, rzecz jasna, kończy pracę strumienia);

Optional<T>

Innym rodzajem finiszerów są te zwracające Optionala, który stanowi remedium na poniższą dolegliwość:

Optional to opakowanie/pudełko w które wkładamy to co zwrócił nam strumień. Może nie być tam żadnych wartości, bo wszystko utknęło na poprzedzających filtrach, ale ów obiekt-pudełko dostaniemy na 100% dzięki czemu unikniemy wyrzucenia wyjątku NullPointer. Aby dostać się do wnętrza opakowania, trzeba je oczywiście otworzyć metodą get() i tam w/w wyjątek już może wyjść, ale i na to mamy receptę:

optional.ifPresent(text -> 
System.out.println(text));

Czyli: tylko jeśli w pudełku coś jest, otwórz je i zrób coś z jego zawartością (interfejs Consumer). W przeciwnym wypadku nie rób nic.

Ale, ale – nawet tu pojawia się nam fajna opcja, bo możemy jako końcową metodę strumienia dodać orElse(x), a w nawiasach zamiast x podać obiekt, który zostanie zwrócony jeśli zawartość strumienia okaże się pusta:

Z popularnych metod kończących, których efektem jest zwrócenie Optionala, warto wymienić:

  • findAny() –> zwraca zazwyczaj pierwszy element strumienia
  • findFirst() –> zwraca ZAWSZE pierwszy element strumienia
  • reduce() –> ustawiamy BinaryOperator na podstawie którego cały zestaw elementów strumienia jest redukowany do jednego obiektu, np.
(1).reduce((t1, t2) -> t2);
(2).reduce((t1, t2) -> 
{if (t1.startsWith("m")) return t1; else return t2;});

Kod (1) powyżej zawsze zwróci nam ostatni element strumienia. Natomiast (2) zostawi nam pierwszy element, który zaczyna się na literę „m”.

Collectors

Optionale sprawdzają się, kiedy zależy nam na znalezieniu pojedynczego elementu z kolekcji, albo sprawdzeniu czy w ogóle takowy tam istnieje. Jednakże, pracując z kolekcjami i przepuszczając je przez strumienie najczęściej zostaniemy na końcu z liczbą mnogą elementów. W takich sytuacjach najlepiej sprawdzają się statyczne metody klasy Collectors. Kończymy nasz strumień wywołaniem metody collect() która za parametr przyjmuje jakiegoś Kolektora:

.collect(Collectors.toSet());
.collect(Collectors.toList());
.collect(Collectors.toMap
(Function k, Function v));

Dostajemy także metodę joining, przydatną np. przy łączeniu poszczególnych elementów typu String w jeden duży obiekt String:

Bardzo fajne perspektywy roztacza przed nami metoda groupingBy() dzięki której możemy ustawić ramy (metodą interfejsu Function) wedle których elementy zbioru zostaną pogrupowane. A także jej siostra: partitioningBy(), dzieląca zbiór tylko na dwie grupy: elementy spełniające lub niespełniające zawartego w niej warunku (Predicate).

Boolean

Na koniec jeszcze metody sprawdzające strumień pod względem określonego warunku (Predicate). Kiedy np. chcemy sprawdzić, czy wszyscy nasi użytkownicy podali nr telefonu, albo czy jest tam choć jedna osoba z naszego miasta, tudzież czy nikt przypadkiem nie jest miłośnikiem pizzy z ananasem, wtedy możemy zakończyć nasz strumyk jedną z poniższych metod. Ich nazwy mówią same za siebie.

allMatch()
anyMatch()
noneMatch()

Strumienie -> Podsumowanie

Powyższe metody to oczywiście nie wszystko co oferuje Stream API, ale jak możecie zauważyć, artykuł już i tak spasł się do granic możliwości, dlatego uznajmy to za odpowiednie miejsce do zakończenia tematu.

Co pominąłem?

W sumie, nic szczególnie istotnego:

  • to jak ma zachować się metoda toMap() w przypadku zduplikowanych kluczy (tip: ta metoda ma wersję biorącą 3 parametry)
  • klasę IntSummaryStatistics dającą podgląd na statystyki strumienia zawierającego typ prosty int (są też wersje dla long i double)
  • łączenie kilku strumieni w jeden
  • pracę wielowątkową (parallelStream())

Żadna z powyższych metod nie przytrafia się zbyt często w czasie pracy ze strumieniami, a jeśli już – to mając tak solidne podstawy z pewnością sami dacie radę rozkminić jak się za zabrać za otrzymane zadanie.

Tym optymistycznym akcentem pozwolę sobie zakończyć niniejszy cykl poświęcony programowaniu funkcyjnemu w Javie.

Teraz kilka dni przerwy i bierzemy się za coś jeszcze lepszego!

Tags: functional programming

Continue Reading

Previous: Programowanie funkcyjne – Strumienie (1/2)
Next: Junior Developer AD2024

Related Stories

Mageege Moon104 – test niskoprofilowego mechanika
  • Sprzęt

Mageege Moon104 – test niskoprofilowego mechanika

11 marca 2025
Przebranżowienie cz.4
  • Programowanie

Przebranżowienie cz.4

27 lutego 2025
Smartfon Jutra
  • Sprzęt

Smartfon Jutra

15 lutego 2025

Ze świata

  • Antyweb
  • Kwantowo
  • Dwóch po dwóch
Netflix odpala nową funkcję, która da użytkownikom więcej władzy
Tanie iPhone'y i Samsungi w nowym sklepie Orange
Fani Androida mają powód do radości. Wraca wsparcie uwielbianej funkcji
Wielkie nieporozumienie wokół nowego iPhone’a. Zobaczcie to, zanim zaczniecie krytykować
Chiny przejmują legendarny oddział Sony. Koniec ery
Tak chcemy wyjaśnić napięcie Hubble'a. Lepszej metody dotąd nie było
Netflix odkrywa karty o premierach. Wiemy, co pokaże na dniach
A co powiesz na ładowanie... laserem? Badacze już to testują
ChatGPT zabierze nasze pieniądze? Nowy pomysł jeszcze gorszy niż reklamy
To najmniejsza myszka na świecie. Użyjesz jej wszędzie
Ta aplikacja to „centrum dowodzenia” każdego miłośnika podróży
Kaufland zaszalał. Rozchwytywany sprzęt Parkside trafił na półki
O tych nowościach w Windows 11 jest dziwnie cicho. A szkoda
Microsoft szykuje rewolucję. Skorzystasz, jeśli obejrzysz reklamy
Te zestawy LEGO za moment znikną z rynku. Wiecie, co to oznacza?
To już ostatni dzwonek! Skorzystaj za darmo, zanim Apple to wyłączy
Masz uczulenie na sierść psa? Z tym nic ci nie grozi
Prawdziwa bomba od T-Mobile - klienci Heyah będą zachwyceni!
Garmin, Samsung i Apple na celowniku. Te zegarki mogą zniknąć z rynku
Łatwiej zmienisz przeglądarkę na iPhonie. Wystarczy jedna opcja
To by było na tyle, jeśli chodzi o możliwość ugody
Ocalić od zapomnienia
Ostatni kwant
ALH 84001 – meteoryt, o którym mówiono nawet w Białym Domu
HESS zarejestrował kosmiczny elektron o niespotykanej energii [Phys. Rev. Lett.]
Matka ciemnej materii – recenzja biografii “Vera Rubin. Życie”
Satelita, który zerwał się ze smyczy
Wiadomość od Carla Sagana do przyszłych eksploratorów Marsa
Ile najdłużej może trwać zaćmienie Słońca?
Nowa największa liczba pierwsza ma ponad 41 milionów cyfr [GIMPS]
30 lat konsoli PlayStation – Odcinek #130
Omawiamy serię The Walking Dead (gość: Stary Gracz)
Nikt nie potrzebuje cienkich smartfonów – Odcinek #129
To ostatni dzwonek na kolekcjonowanie gier i filmów
Najlepsza relacja z PGA 2025 (Poznań Game Arena)
Bumblebee wśród klawiatur. Marvo Meqa 80W – recenzja
Pierwsze spotkanie z Omoda 7 Super Hybrid
Logitech MX Master 4, Wednesday, 1670 sezon 2 – Odcinek #128
Tani pad, który chciał być jak DualSense. Test Monka Contra GT-96
Secret Service i prasa komputerowa w Polsce – Odcinek #127

To może cię zainteresować:

Mageege Moon104 – test niskoprofilowego mechanika
  • Sprzęt

Mageege Moon104 – test niskoprofilowego mechanika

11 marca 2025
Przebranżowienie cz.4
  • Programowanie

Przebranżowienie cz.4

27 lutego 2025
Smartfon Jutra
  • Sprzęt

Smartfon Jutra

15 lutego 2025
Czym jest Swagger?
  • Programowanie

Czym jest Swagger?

22 lipca 2024
  • Technologie jutra
  • Sprzęt jutra
  • Aplikacje jutra
  • Programowanie
  • Księga Drogi
  • Renowacja
  • Różności
  • Archiwum
  • Autor
Copyright © All rights reserved. | DarkNews by AF themes.