Programowanie funkcyjne jest możliwe w Java od wersji 8. Jednak stanowi ono tylko wsparcie do wykonywania określonych czynności bez spełnienia wszystkich założeń czysto funkcyjnych języków programowania jakim jest np. Haskell.
Niemniej API programowania funkcyjnego świetnie sprawdzają się do przetwarzania kolekcji, oraz jego wykorzystanie jest bardzo ważne przy programowaniu reaktywnym.
Programowanie funkcyjne a obiektowe
Często można usłyszeć, że programowanie funkcyjne stanowi następstwo programowania zorientowanego obiektowo.
Nie jest to do końca prawdą, chociaż z punktu widzenia poziomów języków programowania, to rzeczywiście języki programowania takie jak C/C#/Java itp. są językami imperatywnymi (klasyfikowanymi jako języki III poziomu). Języki wykorzystujące model programowania deklaratywnego np. SQL/Haskell są językami deklaratywnymi (klasyfikowanymi jako języki IV poziomu).
Programowanie imperatywne a deklaratywne
Zestawienie porównujące ich kluczowe założenia:
Programowanie imperatywne | Programowanie deklaratywne |
Opisujesz co ma zostać zrobione | Opisujesz co chcesz osiągnąć |
Typy mutowane | Typy niemutowane |
Przekazywanie typów | Przekazywanie typów i funkcji |
Wątkowo-niebezpieczne (Not thread safety) | Wątkowo-bezpieczne (Thread safety) |
Przede wszystkim założeniem języków deklaratywnych jest zadeklarowanie (stąd też nazwa) co chcemy osiągnąć, a nie jak osiągnąć. Więc my rozkazujemy, a niech kompilator się martwi jak to osiągnąć.
Obiekty niemutowane
W programowaniu funkcyjnym nie korzystamy z typów mutowalnych, czyli takich, które mogą zmienić swoją wartość.
Od razu sprostuje, że może tu nasuwać się podobieństwo jakie dostarcza słowo kluczowe final – jednak należy pamiętać, że:
- final – może mieć jedną inicjalizację;
- obiektowi final można zmienić wartości pól;
- obiekt final może być listą elementów, które możemy swobodnie modyfikować.
Finalnie final nie ma nic wspólnego z niemutowalnością 🙂
Programowanie funkcyjne w Java – praktyka
Programowanie funkcyjne w Java odbywa się z wykorzystaniem strumieni. Strumienie to zbiór danych i metod jakie można wykonać.
Aby dobrze to zrozumieć zróbmy proste zadanie. Naszym celem jest policzenie ilości imion znajdujących się na liście, których długość jest równa sześciu znaków.
W klasycznym podejściu tworzymy pętle w której będziemy dokonywać sprawdzenia czy imię ma 6 znaków i jeśli tak to inkrementujemy licznik, którego wartość wyświetlamy na zakończenie pętli.
List<String> names= Arrays.asList("Przemek", "Dorota", "Łukasz", "Karol", "Anna", "Marcysia"); long counter = 0; for (String name : names) { if (name.length() == 6) { counter++; } } System.out.println(counter);
W programowaniu funkcyjnym na liście wywołujemy metodę stream(), która dostarcza nam szereg operacji jakie możemy wykonać w ramach kolekcji. Przykładem jest filter, który selekcjonuje elementy zgodnie z podanym warunkiem.
Do operacji filter przekazujemy nazwę elementu bez podawania jego typu, ponieważ typ jest wnioskowany. Java jest w stanie się domyślić, że skoro przetwarzamy strumieniowo listę String’ów, to typem wykorzystanym w filter jest String. Następnie podajemy strzałkę i warunek. Na zakończenie wykonujemy count, by otrzymać ilość wyników spełniających warunek.
List<String> names= Arrays.asList("Przemek", "Dorota", "Łukasz", "Karol", "Anna", "Marcysia"); long count = names.stream().filter(name -> name.length() == 6).count(); System.out.println(count);
Zapis jest dużo krótszy, a największą korzyścią jest brak konieczności zmieniania stanu obiektu przy każdym obrocie pętli dla zmiennej count jak ma to miejsce w podejściu klasycznym.
Operacje dostarczane przez strumienie – szkolenie w pigułce
Podobnie Java Stream dostarcza szereg innych metod, które w bardzo łatwy sposób pozwolą przetwarzać Ci zbiory danych. Aby zapoznać się z ich pełnymi możliwościami zapraszam Cię na część drugą w postaci wideo, ponieważ w ten sposób najprościej przedstawię Ci wszystko to co dostarcza API programowania funkcyjnego.
Kilka mitów o programowaniu funkcyjnym
„Programowanie funkcyjne jest szybsze”
Java nie została zaprojektowana z myślą o programowaniu funkcyjnym. Dlatego przetwarzanie danych wykorzystując stream’y w większości przypadków jest mniej wydajne. Przykład z imionami jaki podałem powyżej najpewniej wykona się szybciej w implementacji deklaratywnej. I ten fakt musimy mieć na uwadze zwłaszcza przetwarzając duże zestawy danych. Jednak korzyścią płynącą z programowania funkcyjnego jest mniejsze zaangażowanie pamięci.
„Programując funkcyjnie mamy czytelniejszy kod”
Bardzo sporna kwestia, bo tyczy się gustu, ale patrząc z punktu widzenia osoby, która zarządza zespołem programistów to musimy pamiętać, ze w zespole mogą być osoby, które nie programowały w Java 8+ / programują w niej od niedawna lub mają z nią bardzo wąską styczność jak to bywa w zespołach cross-dyscyplinarnych, które są coraz bardziej popularne i sam w takim pracuje. Składnia nie jest oczywista, trzeba z nią się obyć, więc czasem nie warto komplikować. Tutaj jedynie przestrzegam przed przepisywaniem całego projektu lub robienia wstawek często na siłę.
Programowanie funkcyjne – co dalej?
Podsumowując programowanie funkcyjne to bardzo zbawienne narzędzie, które znacznie uprosiło życie programistom Java. Wcześniej programiści wiele rzeczy musieli implementować na piechotę, albo posiłkować się biblioteką Guava. Jeśli już o bibliotekach mowa, to polecam przetestować bibliotekę Vavr, która rozszerza standardowe API programowania funkcyjnego 🙂
A jak z programowaniem funkcyjnym u Ciebie? Stosujesz i jak się z tym czujesz? 🙂