
Programowanie funkcyjne jako idea, wyrażenia lambda, odniesienia do metody… niby to już wszystko co potrzebne nam będzie do puszczenia silnego, zdrowego strumienia. Tylko dlaczego wydaje mi się jakbym o czymś zapomniał?
Odcinek o lambdzie rozpocząłem smutną historią nieszczęśliwej miłości i matczynego poświęcenia. Jeśli miałbym ochotę na powtórzenie tego formatu w dzisiejszym artykule, to motywem przewodnim byłaby z pewnością:
Zazdrość
W życiu każdego początkującego backendowca przychodzi ten moment kiedy musi dotknąć baz danych. Najpierw robi to dwumetrowym kijem, ostrożnie zza winkla, patrząc co się stanie. Potem znajduje krótszy patyk, kuca obok tabeli i z zaciekawieniem gmera, obserwując rezultaty swoich poczynań. Ostatecznie wkłada w tego SQL-a obie dłonie aż po łokcie i z mieszaniną odrazy oraz fascynacji patrzy jak tłuste zapytania przeciekają mu przez palce. Babra się w kolumnach i rzędach jak małe dziecko w błocie, z niechęcią spoglądając na zegarek, bo w końcu jednak trzeba będzie wrócić do logiki programu.

Firma Oracle, stojąca za sukcesem komercyjnym Javy to również ta sama korporacja co wykupiła MySQLa oraz stworzyła własny system bazodanowy (nazwany po prostu „Oracle„). Aby zrozumieć czym są i jak działają strumienie musimy wpierw wyobrazić sobie jak doszło do ich powstania. W tym celu potrzebny nam będzie „John„. Jest on hipotetycznym pracownikiem Oracle w dziale Database, siedzi sobie wygodnie w swoim boksie i wyszukuje w bazie danych to co mu każą:
- Przeszukaj bazę komputerów naszych pracowników, znajdź wszystkie, które mają więcej jak 5 lat i pobierz ich lokalizacje – wyślemy tam serwisanta, który dołoży pamięci. A, no chyba, że to Maki, wtedy zaznacz w bazie, że do wymiany.
- Weź tabele z opiniami testerów o wersji beta 1.1 i 1.2 aplikacji XXX i znajdź negatywne feedbacki, które się powtarzają w obu. Ale tylko w wersji na rynek azjatycki!
John jedną ręką wpisuje zapytanie, drugą trzymając telefon z włączoną aplikacją randkową scrolluje potencjalne kandydatki na żonę. Zez rozbieżny bardzo mu w tym pomaga. Jak również natura jego pracy, która jest łatwa, wygodna i powtarzalna.

Następują przetasowania w firmie. Część działu Database ląduje na bruku, ale nasz John ma szczęście i został „tylko” przesunięty do departamentu programistów Javy. Od teraz będzie pisał logikę oprogramowania.
John z oczywistych powodów nie jest zachwycony nowym otoczeniem, ale szybko się wdraża, poznaje potrzebne biblioteki, rozgryza szczegóły i zaczyna pisać kod. Nie ma już jednak dostępu do produkcyjnej bazy danych, więc testy musi pisać korzystając ze zwykłych javowych kolekcji. Pojawiają się pierwsze ify… else-if… bloki synchronizowane… zmienne ulotne… Proste zapytanie, które w SQL-u zajęłoby mu chwilę, teraz rozrasta się do kilkunastu linijek, gdzie na końcu nie wiadomo co jest czym.
John już rozumie w co się wpakował. Wstaje z fotela i pyta się ogółu:

Tak właśnie żyli programiści Javy do 2014 roku kiedy pojawiła się wersja ósma, a z nią Stream API pozwalające na szybkie i bezpieczne w pracy wielowątkowej przesiewanie kolekcji (i nie tylko).
Nie mam najmniejszych wątpliwości, że motywem przewodnim prac nad Stream API była zazdrość backendowców patrzących na to z jaką gracją specjaliści od baz danych piszą swoje zapytania.
Ci z was, którzy już mieli do czynienia z SQL-em mogą mieć teraz całkiem poprawne rozumowanie tego jak ten system działa. A reszta?
Wyobraźcie sobie woreczek. Nasypiemy do niego jakichś pomieszanych ziarenek, może to być groch z kukurydzą, ważne aby fajnie grzechotało jak się tym bawić. A wiecie, co – dodajmy tam też takich małych żelaznych kulek! I tych styropianowych też!
Woreczek trzeba dobrze zamknąć i zawiesić wysoko, tak aby kot nie dosięgnął łapką, bo jak chwyci to schowa gdzieś pod kanapą i nici z przykładu.
Następnie będą nam potrzebne: jakiś pojemnik (może być drugi worek albo kubek), sitko i scyzoryk.
Teraz od spodu, w tym wiszącym worku wycinamy scyzorykiem dziurę. Towar oczywiście zaczyna się wysypywać, więc podstawiamy sitko, a pod nim pojemnik. Telepiemy sitem i na samym końcu zostają nam tam tylko największe ziarenka: grochu. Cała reszta przeszła przez sitko do kolejnego pojemnika.
Ale to jeszcze nie koniec. Teraz bierzemy porządny magnes i mieszamy nim w tym drugim pojemniku. Cyk – wszystkie metalowe kulki zebrane! Został sam ryż ze styropianem. A zależy nam na ryżu, bo zbliża się pora obiadu.

Wlewamy do garnka wodę, wsypujemy tam zawartość pojemnika i problem sam się rozwiązuje – ryż opada na dno, a leciutki styropian pływa po powierzchni, gotowy do zebrania.
W taki właśnie sposób działają strumienie w Javie
Ważne aby uzmysłowić sobie, że choć metoda może wyglądać jakby tam był tylko jeden strumień, to w rzeczywistości po każdym „filtrowaniu” pojawia się nowy obiekt strumienia i to właśnie na nim wywoływana jest kolejna metoda.
Oryginalny strumień zawierał mieszankę ryżu, grochu, metalu i styropianu, Drugi już był pozbawiony grochu, a ostatni miał tylko styropian i ryż. Ustawiając swoje sitka musimy zatem pamiętać o właściwej kolejności. A same filtry też trzeba jakoś zdefiniować – np. na podstawie wielkości ziaren, albo ich właściwości fizycznych.
List<String> names = List.of("John", "Anna", "Michael");
return names.stream()
.filter(t -> t.contains("a"))
.collect(Collectors.toList());
W poprzednich wpisach z tej serii już kilka razy mówiłem, że całe to programowanie funkcyjne w Javie opiera się na idei interfejsów z jedną metodą abstrakcyjną, która implementowana jest na szybko, tam i w ten sposób jaki akurat jest potrzebny.
To o czym jeszcze nie wspominałem (a czas najwyższy!), to fakt, że te interfejsy zostały już przygotowane przez twórców Javy i nie musimy wymyślać ich od nowa. Lepiej – to właśnie na tych predefiniowanych interfejsach opierają się metody Stream API!
Owe interfejsy funkcyjne różnią się od siebie przyjmowanymi parametrami i zwracanymi typami swoich metod. Na szczęście mają dość intuicyjne nazwy, więc nawet jeśli nie nauczymy się ich na pamięć to i tak będziemy mogli wywnioskować do czego się ich używa, jakie parametry są przyjmowane i co jest zwracane*. Np. interfejs Supplier (and. Dostawca) nie bierze żadnych parametrów, ale coś zwraca, za to jego brat Consumer (ang. Konsument) – przeciwnie – pobiera parametr, ale nic nie daje od siebie.
(*) to akurat łatwe, bo tam wszędzie używa się typów generycznych, więc mamy tylko dwie opcje – albo metoda nie zwraca nic, albo zwraca ten sam typ, który widnieje w klamrach generyka.
Wypunktujmy sobie interfejsy używane w javowych strumieniach:
Comparator<T>
Metoda compare() przyjmuje dwa parametry tego samego typu, ale zawsze zwraca liczbę typu int. Służy do porównywania ze sobą dwóch obiektów na podstawie ich właściwości mierzalnych w liczbach. Możemy też użyć domyślnej metody reversed(), która zwraca odwrotny wynik.

Consumer<T>
W metodzie accept() przyjmuje 1 parametr typu T ale nic nie zwraca. Co ciekawe ten interfejs ma również zaimplementowaną metodę domyślną andThen(), która przyjmuje Consumera i zwraca Consumera, dzięki czemu możemy ustawić co się dzieje z przyjętym parametrem później.

Supplier<T>
Ma tylko jedną metodę get() nieprzyjmującą żadnych parametrów, ale za to zwracającą typ T.

Predicate<T>
Z angielskiego „predicate” oznacza „stwierdzenie”, a to może być albo prawdziwe albo fałszywe. To właśnie jedną z tych wartości zwraca metoda test() tego interfejsu – w oparciu o jeden parametr typu T. Są również metody domyślne: and(), or() albo not() pozwalające łączyć orzeczenia na wzór tego co możemy umieścić w „ifie” używając znaków &&/||/!=.

Function<T,R>
Interfejs „Funkcja” to taka maszynka, że wrzucamy tam jeden obiekt (typu T), a z metody apply() wychodzi drugi – inny (typu R). Możemy też użyć metody domyślnej identity(), że wychodzi dokładnie to co zostało wrzucone. Albo przygotować bardziej złożoną procedurę łącząc kilka funkcji w jedną za pomocą metody compose().

Interfejsy typu Operator
UnaryOperator<T> przyjmuje zmienną typu T, coś z nią robi i wypuszcza rezultat tego samego typu. BinaryOperator<T> to w sumie to samo, ale zamiast jednego, bierze dwa parametry tego samego typu.

Runnable
Runnable zostawiłem na koniec, bo to taki interfejs, który już niby znamy z wielowątkowości, ale jak się okazuje – nadaje się też do operowania strumieniami. Jego metoda run() nic nie pobiera, ale też nic nie zwraca.

Koniec części pierwszej
W początkowym założeniu chciałem, aby cały tekst o strumieniach zmieścił się w jednym artykule, ale w trakcie pisania wykalkulowałem, że albo wpis będzie suchy i z samymi ogólnikami, albo udam, że interfejsy funkcyjne nie są aż takie ważne i pominę ten fragment. Lub też, że treści będzie na tyle dużo, że połowa zainteresowanych się przestraszy i zostawi tekst na później, które nigdy nie nadejdzie (byłem tam, znam to…).
W związku z takim dylematem postanowiłem podzielić całość na dwie części. Dzisiaj odpowiedzieliśmy na pytania: czym jest oraz jak działa strumień w Javie, a także z jakich interfejsów korzystają jego metody.
Natomiast już za kilka dni pojawi się druga część w której zaprezentuję w jaki sposób z takiego strumienia korzystać – jakich metod często się używa i jak zebrać owoce swojego dzieła.
Jak zwykle, dziękuję za uwagę. Kod użyty w tym wpisie znajdziecie na moim GitHubie.