45
1 Aplikacje w Javie – wykład 13 Elementy programowania funkcyjnego Treści prezentowane w wykładzie zostały oparte o: Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014 http://docs.oracle.com/javase/8/docs/

Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

1

Aplikacje w Javie – wykład 13

Elementy programowania funkcyjnego

Treści prezentowane w wykładzie zostały oparte o:● Barteczko, JAVA Programowanie praktyczne od podstaw, PWN, 2014● http://docs.oracle.com/javase/8/docs/

Page 2: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

2

Programowanie funkcyjne - wprowadzenie● Języki programowania takie jak C/C++/Java/Python są nazywane

imperatywnymi językami programowania, ponieważ zawierają sekwencje zadań do wykonania. Programista jawnie, krok po kroku definiuje jak wykonać zadanie. Programowanie funkcyjne działa inaczej. Zamiast sekwencyjnie wykonywać zadania, języki funkcyjne wyznaczają jedynie wartości poszczególnych wyrażeń.

● Programy funkcyjne składają się jedynie z funkcji. Funkcje są podstawowymi elementami języka funkcyjnego. Główny program jest funkcją, której podajemy argumenty, a w zamian otrzymujemy wyznaczoną wartość – wynik działania programu. Główna funkcja składa się tylko i wyłącznie z innych funkcji, które z kolei składają się z jeszcze innych funkcji. Funkcje takie dokładnie odpowiadają funkcjom w czysto matematycznym znaczeniu – przyjmują pewną liczbę parametrów i zwracają wynik. Każda operacja wykonywana podczas działania funkcji, a nie mająca związku z wartością zwracaną przez funkcję to efekt uboczny (np. operacje wejścia wyjścia, modyfikowanie zmiennych globalnych). Funkcje które nie posiadają efektów ubocznych nazywane są funkcjami czystymi (pure function).

Page 3: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

3

Programowanie funkcyjne - wprowadzenie

● Ideą programowania funkcyjnego jest pisanie programów w kategoriach "co ma być osiągnięte", a nie - jak w programowaniu imperatywnym - poprzez specyfikowanie kolejnych kroków algorytmu do wykonania. Takie podejście jest możliwe, gdy w języku programowania możemy traktować fragmenty kodu (funkcje) jako pełnoprawne obiekty, które mogą być przekazywane innym funkcjom i zwracane z innych funkcji. W czystych językach funkcyjnych tak właśnie się dzieje, co więcej - funkcje nie zmieniają żadnych danych (stanów), ważne jest jedynie wyliczanie ich wyników na podstawie podanych argumentów (co ma duże znaczenie np. przy przetwarzaniu równoległym).

● Rozważmy przykład. Mamy listę liczb src. Na jej podstawie utworzyć listę target zawierającą kwadraty tych liczb z src, które są mniejsze od 10.

List<Integer> src = Arrays.asList(5, 72, 10, 11, 9); List<Integer> target = new ArrayList<>(); for (Integer n : src) { if (n < 10) target.add(n * n) }

Ale kod nie jest zbyt ogólny, uniwersalny. Jest imperatywny - bo musieliśmy dokładnie zapisać każdy krok naszego "algorytmu". Co się stanie, jeśli zmienimy warunek selekcji liczb (np. biorąc pod uwagę tylko liczby parzyste) albo operację, która na wybranych liczbach ma być wykonana (np. zamiast podniesienia do kwadratu - dodanie jakiejś liczby)? Musielibyśmy za każdym razem pisać podobne pętle jak wyżej.

Page 4: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

4

Programowanie funkcyjne - wprowadzenie

● A gdyby trzeba było z listy napisów wybrać te o określonej długości i przekształcić je np. do wielkich liter? Znowu trzeba by było pisać podobne pętle, tym razem operujące na listach napisów.

● A przecież zawsze chodzi tu o wybór elementów listy src do przetwarzania, wykonanie na nich określonej operacji i dodanie wyników do listy target. Przydałaby się pewnie metoda create, której moglibyśmy używać np. tak:

List<jakiś_typ> target = create(src, tu podać warunek wyboru elementów z listy src, tu podać operację na wybranych elementach);

To już przypomina styl funkcyjny: określamy tylko co chcemy osiągnąć i - najczęściej - nie musimy pisać imperatywnego kodu.

● No ale jak podać warunek wyboru i operację? Jasne jest, że to muszą być jakieś fragmenty kodu, które wykonują odpowiednie obliczenia i zwracają wyniki. Czyli - ogólnie mówiąc - funkcje, które przekazujemy metodzie create, a ona będzie je wywoływać.

● W Javie przed wersją 8 moglibyśmy coś takiego zrobić korzystając z interfejsów (widzieliśmy to już niejednokrotnie, gdy np. przekazywaliśmy kod do wykonania w wątku jako obiekt implementujący interfejs Runnable czy Callable, albo podając komparatory (obiekty implementujące interfejs Comparator) przy sortowaniu list.) (np. przez wykorzystanie wzorca projektowego Strategia).

Page 5: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

5

Programowanie funkcyjne - wprowadzenie

Zdefiniujmy interfejs Filter, którego metoda test() będzie odpowiedzialna za wybór elementów listy (ogólniej - zwraca true, jeśli przekazana jej wartość spełnia zapisane w kodzie tej metody warunki, false - w przeciwnym razie):

public interface Filter<V> { boolean test(V v);}

oraz interfejs Transformer, którego metoda transform() ma wykonać operację na przekazanej wartości i zwrócić jej wynik:

public interface Transformer<T,S> { T transform(S v);}

Napisanie metody create() w kategoriach tych interfejsów jest całkiem łatwe

static <T, S> List<T> create(List<S> src, Filter<S> f, Transformer<T, S> t) { List<T> target = new ArrayList<>(); for (S e : src) if (f.test(e)) target.add(t.transform(e)); return target;}

Page 6: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

6

Programowanie funkcyjne - wprowadzenie

W programach, pisanych w Javie przed wersją 8, takiej metodzie create(...) trzeba było przekazywać obiekty implementujące podane interfejsy, np:

List<Integer> src = Arrays.asList(5, 72, 10, 11, 9);List<Integer> target = create(src, new Filter<Integer>() { public boolean test(Integer n) { return n < 10; } }, new Transformer<Integer, Integer>() { public Integer transform(Integer n) { return n*n; } });

To już jest trochę w stylu funkcyjnym, ale mało czytelne, obarczone liniami kodu, które nic nie wnoszą (tworzenie obiektów anonimowych klas wewnętrznych tylko po to, by przekazać kod do wykonania). Tak naprawdę ważne są tu przecież tylko dwie linijki:

return n < 10;

ireturn n*n;

Page 7: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

7

Programowanie funkcyjne - wprowadzenie

M.in. dlatego w Javie w wersji 8 wprowadzono, co prawda w ograniczonym zakresie, wsparcie dla programowania funkcyjnego. Obejmuje ono przede wszystkim lambda-wyrażenia oraz przetwarzanie strumieniowe.

Zanim poznamy lambda-wyrażenia w szczegółach, możemy o nich myśleć jako o bezpośrednio podawanych fragmentach kodu, swoistych funkcjach bez nazwy, które są traktowane "jak prawdziwe obiekty", czyli np. mogą być przekazywane metodom. Dzięki zastosowaniu lambda-wyrażeń poprzedni przykład wywołania metody create() upraszczamy praktycznie do jednej linijki:

List<Integer> target = create(src, n -> n < 10, n -> n*n);

Tu zapis: n -> n < 10

jest lambda-wyrażeniem, które możemy traktować jako anonimową funkcję, której parametrem jest n, i która zwraca wynik wyrażenia n<10.

Nie wchodząc na razie w szczegóły, można powiedzieć, że kompilator "dopasowuje" to lambda-wyrażenie do drugiego parametru (Filter<S>) metody create(...), w wyniku czego użyta tam metoda test(...) interfejsu Filter "pod spodem" uzyskuje odpowiednią implementację: boolean test(Integer n) { return n < 10; }

Podobnie dzieje z lambda-wyrażeniem: n -> n*n

pasującym do wywołania metody transform(...) interfejsu Transformer.

Page 8: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

8

Programowanie funkcyjne - wprowadzenie

Przykłady wykorzystania lambda-wyrażeń w różnych kontekstach:

List<Integer> num = Arrays.asList( 1, 3, 5, 10, 9, 12, 7);List<String> txt = Arrays.asList("ala", "ma", "kota", "aleksandra", "psa", "azora" );List<Employee> emp = Arrays.asList( new Employee("Kowal", "Jan", 34, 3400.0), new Employee("As", "Ala", 27, 4100.0), new Employee("Kot", "Zofia", 33, 3700.0), new Employee("Puchacz", "Jan", 41, 3600.0) ); System.out.println( create(num, n-> n%2!=0, n->n*100));System.out.println( create(txt, s -> s.startsWith("a"), s -> s.toUpperCase() + " " + s.length()));

Page 9: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

9

Programowanie funkcyjne - wprowadzenie

List<Employee> doPodwyzki = create(emp, e -> e.getAge() > 30 && e.getSalary() < 4000, e -> e );System.out.println("Podwyzki powinni uzyskac:");System.out.println(doPodwyzki);

Uwaga: w powyższym kodzie wykorzystano prostą klasę Employee z polami lname (nazwisko), fname (imię), age (wiek) i salary (pensja) oraz metodą toString() zwracającą nazwisko i imię.

Otrzymamy następujące wyniki:

[100, 300, 500, 900, 700][ALA 3, ALEKSANDRA 10, AZORA 5]Podwyzki powinni uzyskac:[Kowal Jan, Kot Zofia, Puchacz Jan]Jak widać, stworzona wcześniej metoda create() wykorzystywana jest tu dosyć elastycznie (dla różnych typów danych, różnych warunków, różnych operacji), a dzięki lambda-wyrażeniom sformułowanie tego, co chcemy osiągnąć, jest bardzo proste i czytelne.

Page 10: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

10

Programowanie funkcyjne - wprowadzenie

Jednak metoda create(...) nie do końca jest uniwersalna. Być może chcielibyśmy najpierw przetworzyć dane, a filtrować wyniki tego przetwarzania. A może filtrować, przetwarzać i znowu filtrować?

Te wszystkie przypadki możemy łatwo obsłużyć za pomocą przetwarzania strumieniowego. Pokażemy teraz szczególne przypadki jego użycia, dobrze zastępujące i znacznie poszerzające funkcjonalność, stworzonej wcześniej metody create(...) i to bez konieczności definiowania jakichkolwiek własnych interfejsów.

W kontekście wcześniejszych przykładowych kodów, sekwencja działań jest następująca. Od listy metodą stream() uzyskujemy tzw. strumień (inaczej zwany sekwencją), a na danych strumienia możemy wykonywać m.in. operacje przetwarzania (metoda map), filtrowania (metoda filter) oraz uzyskiwać nowe kolekcje wyników tych operacji (metoda collect).

Metodzie map podajemy lambda-wyrażenie, którego wynikiem jest przetworzenie jego parametru (działa to tak jak transform() z naszego interfejsu Transformer). Metoda zwraca strumień, na którym można wykonywać dalsze operacje. Metodzie filter podajemy lambdę przekształcającą parametr w wartość boolowską (tak jak nasz filter()). Filtrowanie zwraca strumień, który pozwala wykonywać ew. dalsze operacje tylko na tych danych, dla których wynik lambdy jest true. Możemy zatem wywoływać sekwencje map-filter, realizujac kolejne etapy przetwarzania strumienia danych. Metoda collect natomiast kończy przetwarzanie strumienia i ma za parametr obiekt klasy Collector, który tworzy nową kolekcję i dodaje do niej dane ze strumienia, na rzecz którego została wywołana. W szczególności taki kolektor, budujący listę, można uzyskać za pomocą statycznej metody Collectors.toList().

Page 11: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

11

Programowanie funkcyjne - wprowadzenie

Możemy zatem zapisać poprzedni przykład uzyskania listy kwadratów tych elementów listy src, które są mniejsze od 10:

import static java.util.stream.Collectors.toList;// ...List<Integer> src = Arrays.asList(5, 72, 10, 11, 9);List<Integer> target = src.stream().filter(n -> n < 10) .map(n -> n * n) .collect(toList());

Może trochę więcej tu kodu, niż poprzednio, ale za to nie musimy definiowac własnych interfejsów (metody strumieniowe korzystają z interfejsów z pakietu java.util.function, które pokrywają częste przypadki użycia lambda-wyrażeń).

Ale co najważniejsze mamy dużo większą swobodę działania. Możemy np. filtrować kwadraty liczb, czyli najpierw wykonać map(), a później filter():

List<Integer> num = Arrays.asList(1, 3, 5, 10, 9, 12, 7);List<Integer> out = num.stream().map(n -> n*n) .filter(n -> n > 80) .collect(toList());co da w wyniku listę liczb:

[100, 81, 144]

Page 12: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

12

Programowanie funkcyjne - wprowadzenie

Możemy też ulepszyć poprzedni przykładowy fragment, który selekcjonował pracowników do podwyżki. Teraz nie tylko zostanie wybrana odpowiednia grupa pracowników, ale też podwyżki zostaną zrealizowane:

List<Employee> emp = ...emp.stream() .filter(e -> e.getAge() > 30 && e.getSalary() < 4000) .forEach(e -> e.setSalary(e.getSalary()*1.1)); emp.forEach( e -> System.out.printf("%s %.0f\n", e, e.getSalary()));

Ten fragment wyprowadzi na konsolę:

Kowal Jan 3740

Warto zwrócić uwagę, że zastosowano tu dwa razy metodę forEach. Pierwsze jej użycie dotyczyło strumienia zwracanego przez filter(...) i polegało na wykonaniu podanej operacji (ustalenia nowej pensji) na wszystkich elementach zwróconej sekwencji. Takie forEach, tak jak collect, jest jedną z metod, która kończy operacje strumieniowe. Drugie forEach zastosowano wobec kolekcji (listy pracowników) dla wyprowadzenia informacji. Jest to jeden z przykładów nowych (domyślnych) metod w interfejsach kolekcyjnych, którym jako argumenty można podawać lambda-wyrażenia odpowiednich rodzajów.

Page 13: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

13

Interfejsy funkcyjne i lambda-wyrażenia

Jeżeli zestaw metod, deklarowanych w interfejsie, zawiera tylko jedną metodę abstrakcyjną (wyłączając dodane deklaracje abstrakcyjnych publicznych niefinalnych metod z klasy Object), to taki interfejs nazywa się funkcyjnym (SAM - Single Abstract Method).Przykłady interfejsów funkcyjnych:

interface SAMA {

void doWork(int n);//zawiera jedną metodę abstrakcyjną,}

interface SAMB {

void doWork(String s); @Override boolean equals(Object); // przedefiniowuje metodę equals z klasy Object,}

interface SAMC {

void doWork(Collection c);//metoda abstrakcyjna

default public void done() { System.out.println("Done"); }

//metoda domyślna (default), ma implementację}

Page 14: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

14

Interfejsy funkcyjne i lambda-wyrażenia

Interfejsy funkcyjne zostały wybrane jako środek realizacji lambda-wyrażeń w Javie 8. Każde lambda-wyrażenie musi odpowiadać jakiemuś interfejsowi funkcyjnemu i jest typu wyznaczanego przez tego rodzaju interfejsy. W doborze odpowiedniego interfejsu funkcyjnego do lambda-wyrażenia kompilator stosuje deskryptor funkcji (function descriptor) interfejsu funkcyjnego, który opisuje parametry typu (gdy interfejs jest sparametryzowany) oraz typy parametrów, typ wyniku i ew. typ zgłaszanego wyjątku metody interfejsu.Definiując interfejsy funkcyjne, warto stosować adnotację @FunctionalInterface, dzięki której

● kompilator może sprawdzić, czy rzeczywiście dany interfejs jest funkcyjny i poinformować nas o ew. błędach;

● w dokumentacji i kodzie widać od razu, że dany interfejs może być stosowany z lambda-wyrażeniami.

Składnia lambda-wyrażenia:

(lista_parametrów) -> ciało_lambda

gdzie

lista_parametrów oznacza rozdzielone przecinkami deklaracje argumentów przekazywanych lambda-wyrażeniu (jak w nagłówku metody);ciało_lambda to jedno wyrażenie lub blok instrukcji, ujęty w nawiasy {}.

Uwaga. Jeśli typy parametrów mogą być określone przez kompilator w wyniku konkludowania typów (typy inferring), to w deklaracji parametrów można pominąć typy, a wtedy, gdy parametr jest jeden, można pominąć nawiasy okalające listę parametrów.

Page 15: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

15

Interfejsy funkcyjne i lambda-wyrażenia

Rozważmy prosty przykład lambda-wyrażenia, które otrzymuje jako argument liczbę całkowitą i zwraca jej wartość powiększoną o 1:(int n) -> { return n+1; }

Aby w ogóle coś takiego zapisać w programie, musimy mieć odpowiedni interfejs funkcyjny.

Załóżmy, że mamy następujący interfejs (widoczny w kontekście kompilacji):

interface IntProcessor {

int process(int n);

}

Wtedy tego lambda-wyrażenia możemy użyć wszędzie tam, gdzie może wystąpić typ IntProcessor, czyli m.in. w deklaracji zmiennej, przy przypisaniach czy przy przekazywaniu argumentów i zwrocie wyników.

Page 16: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

16

Interfejsy funkcyjne i lambda-wyrażenia

//Użycie lambda-wyrażeń w przypisaniach, przy przekazywaniu //argumentów i zwrocie wyników z metod

class LamSample {

// ...

static void arrOp(int[] arr, IntProcessor p) { //...

}

static IntProcessor getIntProcessor() {

return (int n) -> { return n + 1; }; //lub: n -> n + 1

}

public static void main(String[] args) {

IntProcessor p = (int n) -> { return n + 1; }; // 1

int[] arr = {1, 2, 3};

arrOp(arr, (int n) -> { return n + 1; } ); // 2

IntProcessor pl = getIntProcessor(); // 3

// ... }}

Page 17: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

17

Interfejsy funkcyjne i lambda-wyrażenia

Podanie lambda-wyrażeń w wierszach //1, //2, //3 jest swoistą deklaracją i nie powoduje jeszcze wykonania instrukcji ciała lambdy. Obliczenie lambda-wyrażenia nastąpi dopiero podczas wywołania metody interfejsu IntProcessor. Ale skąd ma się wziąć jej implementacja? Otóż, przy przetwarzaniu kodu źródłowego, kompilator natykając się na lambda-wyrażenie, określa jego typ (tzw. target type) na podstawie kontekstu użycia. W przykładzie jest to proste:

● w wierszu //1 typem lambda-wyrażenia jest IntProcessor, bo taka jest deklaracja zmiennej;

● w wierszu //2 typem jest IntProcessor, bo tak jest określony drugi argument metody arrOp.

Jednocześnie kompilator dodaje do klasy kod, który w fazie wykonania programu wesprze utworzenie odpowiedniej implementacji metody interfejsu funkcyjnego. W naszym przypadku

int process(int a) {return a + 1;}

Wykonanie (obliczenie) lambda-wyrażenia odbywa się przez wywołanie tej metody.

Page 18: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

18

Interfejsy funkcyjne i lambda-wyrażenia

//Wykonanie lambda-wyrażeń

static void arrOp(int[] arr, IntProcessor p) {

for (int i = 0; i < arr.length; i++) {

arr[i] = p.process(arr[i]); // wykonanie lambda

}

}

public static void main(String[] args) {

IntProcessor p = (int n) -> { return n + 1; };

int x = p.process(1); // wykonanie lambda

System.out.println(x);

int[] arr = {1, 2, 3};

arrOp(arr, (int n) -> { return n + 1; } );

System.out.println(Arrays.toString(arr));

}

W wyniku otrzymamy:

2[2, 3, 4]

Page 19: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

19

Interfejsy funkcyjne i lambda-wyrażenia

Mamy tu do czynienia z lambda-wyrażeniem, które zwraca wynik. Mogą być też takie, które wyników nie zwracają, wtedy typem ich wyniku jest void (lub typ kompatybilny z void). Nasze przykładowe lambda-wyrażenie możemy znacznie uprościć. Kompilator może konkludować o typie parametru, więc w lambdzie możemy pominąć typ parametru. Możemy pominąć również () i {}.

arrOp(arr, n -> n+1); arrOp(arr, n -> n*10);

Wyobraźmy sobie teraz, że oprócz interfejsu IntProcessor w kontekście kompilacji jest też widoczny interfejs funkcyjny StringProcessor. Dodajmy metodę, która wykorzystuje go do przetwarzania tablicy napisów

interface IntProcessor { int process(int n); }interface StringProcessor { String process(String n); }public class Sample { static void arrOp(int[] arr, IntProcessor p) { for (int i = 0; i < arr.length; i++) { arr[i] = p.process(arr[i] );} } static void sarrOp(String[] arr, StringProcessor p) { for (int i = 0; i < arr.length; i++) { arr[i] = p.process(arr[i]); } } public static void main(String[] args) {//.... }}

Page 20: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

20

Interfejsy funkcyjne i lambda-wyrażenia

Jakiego typu jest lambda-wyrażenie: n -> n + 1?

Zależy to od kontekstu użycia. Gdy w metodzie main napiszemy np.

int[] arr = {1, 2, 3}; arrOp(arr, n -> n+1);

to na podstawie konkludowania typów lambda-wyrażenie będzie typu IntProcessor, a gdy napiszemy

String[] sarr = {"Ala", "kot", "pies"}; sarrOp(sarr, n -> n+1);

to, w wyniku konkluzji, typem tego lambda stanie się StringProcessor.

Mamy tu do czynienia z wyrażeniami wielotypowymi (polytype expressions), czyli takimi, które mogą mieć różne typy w zależności od kontekstu użycia. Lambda-wyrażenia stanowią jeden z przypadków wyrażeń wielotypowych w Javie. Stanie się to jeszcze bardziej widoczne (i bardziej funkcjonalne), gdy nieco uogólnimy kody, stosując metody sparametryzowane.

Page 21: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

21

Interfejsy funkcyjne i lambda-wyrażenia

//Lambda-wyrażenia - konkludowanie typów

interface Processor<T> { T process(T val); }

public class ArrProc {

public static <T> void processArr(T[] arr, Processor<T> p ){

for (int i = 0; i < arr.length; i++) {

arr[i] = p.process (arr[i]);

}

}

public static void main(String[] args) {

String[] sa = { "a", "b", "c" };

Integer[] ia = (1, 2, 3);

processArr(sa, v -> v+l); // konkludowanie typów

processArr(ia, v -> v+l); // konkludowanie typów

processArr (sa, v -> v.toUpperCase()); // konkludowanie typów

processArr(ia, v -> v*v); // konkludowanie typów

System.out.println(Arrays.toString(sa)) ;

System.out.println(Arrays.toString(ia));

}}//wynik: [A1, B1, C1] [4, 9, 16]

Page 22: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

22

Interfejsy funkcyjne i lambda-wyrażenia

Jak już widzieliśmy, lambda-wyrażenia można stosować jako uproszczenie w użyciu anonimowych klas wewnętrznych, implementujących interfejsy funkcyjne. Podobnie jak w lokalnych klasach wewnętrznych lambda-wyrażenie ma dostęp

● do wszystkich składowych klasy otaczającej, przy czym pola tej klasy mogą być przez lambda-wyrażenie modyfikowane;

● do zmiennych lokalnych, ale muszą one albo być deklarowane ze specyfikatorem final albo być efektywnie finalne (effective final).

Inaczej niż w anonimowych klasach wewnętrznych lambda-wyrażenie nie wprowadza nowego zasięgu widoczności i dostępności, co oznacza m.in., że

● zmienna this jest referencją do obiektu klasy, w której tworzone jest lambda-wyrażenie (w klasie anonimowej this oznacza obiekt tej klasy);

● słowo kluczowe super oznacza nadklasę obiektu (klasy), w której tworzone jest lambda-wyrażenie;

● jeśli lambda-wyrażenie występuje w bloku, niedopuszczalne jest przesłanianie zmiennych lokalnych z bloku otaczającego (w klasie lokalnej anonimowej mamy nowy zasięg widoczności, więc możemy używać tych samych identyfikatorów zmiennych, co w bloku otaczającym).

Page 23: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

23

Referencje do metod i konstruktorówCzęsto definicja lambda-wyrażenia sprowadza się do wywołania metody istniejącej klasy lub utworzenia jej obiektu. Wtedy zamiast wywołania tej metody w lambda-wyrażeniu możemy zastosować referencję do metody lub konstruktora. Są cztery rodzaje takich referencji (symbolicznie przez x oznaczamy jeden parametr lub listę parametrów w nawiasach okrągłych):

● NazwaKlasy::nazwa_metody_statycznej

– Klasa::metStat - odpowiada lambda-wyrażeniu

x -> Klasa.metStat(x)

● NazwaKlasy::nazwa_metody_niestatycznej

– Klasa::met - odpowiada lambda-wyrażeniu x -> x.met()

● zmienna_niestatyczna::nazwa_metody_niestatycznej

– Klasa v;

– v::met - odpowiada lambda-wyrażeniu x -> v.met(x)

● NazwaKlasy::new

– Klasa::new - odpowiada lambda-wyrażeniu x -> new Klasa(x)

Page 24: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

24

Referencje do metod i konstruktorówNaturalnie, liczba i typy parametrów oraz typ wyniku metody (konstruktora), do których używamy referencji, muszą odpowiadać deskryptorowi funkcji jakiegoś interfejsu funkcyjnego (który zostanie dobrany do realizacji lambda-wyrażenia). Najlepiej zobaczyć to na przykładzie. Załóżmy, że w kontekście kompilacji widocze są interfejsy funkcyjne:

interface Transformer<T,S> { T transform(S v);}interface Operator<R,T,S> { R oper(T v1, S v2);}oraz następująca klasa Animal:

class Animal{ String type; public Animal(String type) { this.type = type; } public String toString() { return "Animal{type=" + type + "}"; }}

Page 25: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

25

Referencje do metod i konstruktorówZastosowanie referencji do metod i konstruktorów ilustruje poniższy kod:

public class MetRef {

static <T,S> List<T> create(List<S> src, Transformer<T,S> t){

List<T> target = new ArrayList<>();

for (S s : src) target.add(t.transform(s));

return target;

}

static <R,T,S> List<R> create(List<T> src1, List<S> src2,

Operator<R,T,S> o){

List<R> res = new ArrayList<>();

for (int i = 0; i < src1.size(); i++){

res.add(o.oper(src1.get(i), src2.get(i)));

}

return res;

}

Page 26: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

26

Referencje do metod i konstruktorówpublic static void main(String[] args) { List<String> s = Arrays.asList( "pies", "kot", "ryba" ); // zamiast List out = create(s, e -> e.toUpperCase()); System.out.println(out); //[PIES, KOT, RYBA] // możemy napisać out = create(s, String::toUpperCase); System.out.println(out); [PIES, KOT, RYBA] String text = "pies i kot są w domu, a ryba pływa"; // zamiast out = create(s, e -> text.indexOf(e)); System.out.println(out);//[0, 7, 24] // możemy napisać out = create(s, text::indexOf); System.out.println(out);//[0, 7, 24] //zamiast out = create(s, e -> new Animal(e)); System.out.println(out); //[Animal{type=pies}, Animal{type=kot}, Animal{type=ryba}] // możemy napisać out = create(s, Animal::new); System.out.println(out); //[Animal{type=pies}, Animal{type=kot}, Animal{type=ryba}] }}

Page 27: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

27

Gotowe interfejsy funkcyjne

Jak już wspominaliśmy, najczęściej nie musimy tworzyć własnych interfejsów funkcyjnych. Po pierwsze, w Javie już od lat jest wiele interfejsów typu SAM (np. znany nam FileFilter czy Runnable). Po drugie, w wersji 8 pojawił się pakiet java.util.function, w którym zdefiniowano wiele interfejsów funkcyjnych dla częstych przypadków użycia lambda-wyrażeń, np. Predicate<T>, Function<S, T>, UnaryOperator<T>, Supplier<T>, Consumer<T>(), BiPredicate<U,V>, BiFunction<U,V,R>, BinaryOperator<T>, BiConsumer<U,V>.

Przedstawimy, teraz interfejsy z których już korzystaliśmy:

A)ActionListener. Ma on jedną metodę

void actionPerformed(ActionEvent ae)

B)FileFilter. Ma on jedną metodę boolean accept(File f), wykorzystywaną do selekcji obiektów plikowych np. w trakcie listowania zawartości katalogu metodą listFiles (tu jako argument podajemy właśnie implementację metody accept). Zatem dla uzyskania tablicy plików z danego katalogu dir, mających podane rozszerzenie ext i czas modyfikacji późniejszy od podanego time możemy napisać:

File[] files = new File(dir).listFiles(

f -> f.isFile() && f.getName().endsWith("."+ext)

&& f.lastModified() > time);

Page 28: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

28

Gotowe interfejsy funkcyjne

C)Comparator. Jak pamiętamy, ma jedną metodę int compare(T obiekt1, T obiekt2). Przy porównywaniu i sortowaniu możemy więc używać wygodnych lambda-wyrażeń, np. do uporządkowania listy napisów wg ich długości: List<String> lst = Arrays.asList("ala", "ma", "kota", "i", "pieska"); Collections.sort(lst, (s1, s2) -> s2.length() - s1.length()); System.out.println(lst);Powyższy fragment wyprowadzi: [pieska, kota, ala, ma, i]

D)Runnable. Kod zliczający czas (np. w trakcie jakiejś interakcji użytkownika z programem) za pomocą lambda-wyrażenia, pasującego do deskryptora funkcji (public void run()) moglibyśmy napisać tak, jak pokazuje kod:

Future<?> ftask = Executors.newSingleThreadExecutor().submit( () -> { int sec = 0; while(true) { sec++;

try { Thread.sleep(1000); } catch(InterruptedException exc) { return; } System.out.println(sec/60 + ":" + sec%60); } } );

// ... JOptionPane.showMessageDialog(null, "Close dialog to stop"); ftask.cancel(true);

Page 29: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

29

Gotowe interfejsy funkcyjne

E)Callable. Implementację jego jedynej metody V call() throws Exception można podać jako lambda-wyrażenie. Poniższy kod w odrębnym wątku odwraca napis, co sekundę wypisując kolejną wynikową literę, a główny wątek czeka na cały odwrócony napis i wyprowadza go na konsolę.

ExecutorService ex = Executors.newCachedThreadPool(); Future<String> task = ex.submit (()->{ char[] src = s.toCharArray(); char[] trg = new char[src.length]; for (int i = src.length-1, j=0; i >= 0; i--, j++){ trg[j] = src[i]; System.out.println(trg[j]); Thread.sleep(1000); } return new String(trg);

});String res = task.get();System.out.println(res);Uwaga. Ogólnie odwracanie napisów powinniśmy realizować za pomocą metody reverse klasy StringBuffer lub StringBuilder.

Page 30: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

30

Lambda-wyrażenia w gotowych metodach W Javie 8 pojawiły się nowe metody w standardowych klasach i interfejsach (metody statyczne i/lub domyślne), których argumentami mogą być lambda-wyrażenia. Jest ich całkiem sporo. Omówimy krótko wybrane z nich.

W interfejsie Iterable dodano domyślną metodę forEach(Consumer op) (interfejs Consumer<T> ma metodę void accept(T t) umożliwiającą wykonywanie operacji na przekazanym obiekcie bez zwracania wyniku) dzięki której na dowolnych elementach zestawu danych, określanego przez implementację Iterable (m.in. na dowolnej kolekcji), możemy wykonywać podaną operację op.

Aby przyjrzeć się bliżej działaniu forEach, zdefiniujmy metodę zwiększającą wartości elementów przekazanej listy liczb całkowitych. Argumentem lambda-wyrażenia w forEach będzie kolejny element listy. Nie mamy dostępu do indeksu tego elementu, zatem nie mogąc przeprowadzić modyfikacji za pomocą metody set(i,val) interfejsu List, musimy stworzyć i zwrócić nową listę, która będzie zawierać zmienione wartości :static List<Integer> increase(List<Integer> inList, int n) { List<Integer> out = new ArrayList<>(); inList.forEach(e -> out.add(e+n)); return out;}public static void main(String[] args) { List<Integer> in = Arrays.asList(1, 7, 11, 19); System.out.println(in); //[1, 7, 11, 19] in = increase(in, 2); System.out.println(in); //[3, 9, 13, 21]}

Page 31: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

31

Lambda-wyrażenia w gotowych metodach forEach różni się zdecydowanie od zwykłych pętli (for i rozszerzonego for). Gdybyśmy np. chcieli przerwać zwiększanie o 1 wartości elementów, gdy natrafimy na wartość > 9:

inList.forEach(e -> { if (e > 9) break; out.add(e+n); });

to uzyskalibyśmy błąd w kompilacji. W forEach nie wolno stosować instrukcji break. Również instrukcja return ma inne znaczenie niż zwykle. Nie zwraca sterowania z metody, w której użyliśmy forEach, kończy jedynie wykonanie lambda-wyrażenia dla aktualnie przetwarzanego argumentu. W takim kontekście:

void show(List<Integer> inList) {

inList.forEach(e -> {

if (e == 11) return;

System.out.println(e);

});

}

//...

show(Arrays.asList(1, 7, 11, 19));

na konsoli nie uzyskamy 1, 7 tylko 1, 7, 19. W metodzie forEach instrukcja return przechodzi do następnej iteracji (działa tak jak continue w „normalnych" pętlach).

Page 32: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

32

Lambda-wyrażenia w gotowych metodachAby z lambda-wyrażenia uzyskać zewnętrzną instrukcję return, zwracającą sterowanie z metody, w której to lambda-wyrażenie jest wykonywane, korzystamy z wyjątków:

void show(List<Integer> inList) { try { inList.forEach(e -> { if (e == 11) throw new RuntimeException(); System.out.println(e); }); } catch(RuntimeException exc) { return; }}

Dla inList = [1,7,11,19] na konsoli uzyskamy tylko dwa elementy: 1 i 7. Tutaj dodatkowo zamiast ogólnego RuntimeException należałoby zdefiniować własną klasę wyjątku np. ExternalReturnFromLambda. W przeciwnym razie każdy wyjątek typu RuntimeException, powstający przy wykonaniu operacji w ciele lambda-wyrażenia, powodowałby zakończenie działania otaczającej metody.

Page 33: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

33

Lambda-wyrażenia w gotowych metodachDrobną, acz ciekawą innowacją, jest dodanie domyślnej metody forEachRemaining do interfejsu Iterator, która wykonuje podane lambda-wyrażenie na wszystkich w danym momencie pozostałych elementach (być może po wcześniejszym użyciu metody next() Iteratora).

Utwóżmy listę wynikową, której pierwszy element będzie sumą dwóch pierwszych elementów listy wejściowej, a pozostałe będą zwiększone o 1:

List<Integer> specialOp(List<Integer> inList) { List<Integer> out = new ArrayList<>(); Iterator<Integer> it = inList.iterator(); out.add(it.next() + it.next()); it.forEachRemaining(e -> out.add(e+1)); return out;)// in jest listą [1, 7, 11, 19] in = specialOp(in); System.out.println(in);

Ten fragment kodu wyprowadzi na konsolę: [8, 12, 20].

Naturalnie, gdy forEach iteruje po obiektach klas modyfikowalnych, możemy zmieniać ich stany:

employeeList.forEach(e -> e.setSalary(e.getSalary()*1.2));

Metoda setSalary() ma typ wyniku void (co jest zgodne z kontraktem dla interfejsu Consumer, używanym „pod spodem" lambda-wyrażenia w metodzie forEach), wobec tego mogliśmy zastosować w ciele lambdy pojedyncze wyrażenie. Gdyby metoda zwracała wynik, w lambda-wyrażeniu należałoby użyć bloku instrukcji, z których ostatnią byłoby proste return.

Page 34: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

34

Lambda-wyrażenia w gotowych metodachWygodnie używa się forEach, gdy lambda-wyrażeniem jest referencja do metody niezwracającej wyniku, której argumentem jest obiekt iterowanego zestawu. Sztandarowym przykładem jest wypisywanie informacji na konsolę. Zamiast pisać:

kolekcja.forEach(e -> System.out.println(e));

możemy użyć referencji do metody println:

kolekcja.forEach (System.out::println);

Bardzo użyteczne jest forEach zdefiniowane jako domyślna metoda interfejsu Map. Lambda-wyrażenie jest tu BiConsumerem, czyli ma dwa argumenty (klucz i wartość kolejnego wejścia w mapie) oraz typ wyniku void. Dzięki temu przeglądanie map jest znacznie łatwiejsze, np. zamiast

for(Map.Entry me : map.entrySet()) {

// tu coś robimy z kluczem, uzyskanym przez me.getKey() // i z wartością spod klucza pobraną przez me.getValue()

};

piszemy:

map.forEach ( (key, val) -> robimy coś z key i val );

Page 35: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

35

Lambda-wyrażenia w gotowych metodachIstotne dodatki pojawiły się w interfejsie Comparator. Wiemy już, że tam, gdzie trzeba dostarczyć komparatora możemy podać lambda-wyrażenie. Załóżmy, że mamy klasę Employee z polami: String lname (nazwisko), String fname (imię), Integer age (wiek), Double salary (pensja) z odpowiednimi metodami get zwracającymi wartość danego pola. Mając listę pracowników lemps i używając domyślnej metody sort interfejsu List, możemy sortować listę wg różnych kryteriów, np. wg wieku:

List<Employee> lemps = ...

System.out.println(lemps); // oryginalna lista

lemps.sort((el, e2) -> el.getAge().compareTo(e2.getAge()));

System.out.println(lemps);

po posortowaniu w wyniku dostaniemy:

[Jeż Jan (34,4100.0), Es Joe (41,3600.0), As Ala (27,3700.0)] [As Ala (27,3700.0), Jeż Jan (34,4100.0), Es Joe (41,3600.0)]

Zauważmy, że w lambda-wyrażeniu, reprezentującym komparator, wywołujemy na rzecz obu porównywanych obiektów metodę klasy Employee (getAge), a ponieważ wynikiem jest Integer (implementujący interfejs Comparable), to do porównania wartości możemy zastosować metodę compareTo.

Page 36: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

36

Lambda-wyrażenia w gotowych metodachUżyteczna domyślna metoda removeIf(Predicate) została zdefiniowana w interfejsie Collection (interfejs Predicate<T> ma metodę boolean test(T v)). Usuwa ona wszystkie elementy kolekcji, których wartości spełniają podany w postaci predykatu warunek, np.

Set<String> slist = new HashSet(Arrays.asList( "Cypr – wyspa", "Madagaskar - wyspa", "Krabi", "Londyn"));

slist.removeIf(s -> !s.endsWith("wyspa")); slist.forEach(System.out::println);

da w wyniku:

Cypr - wyspa Madagaskar – wyspa

W standardowych klasach i interfejsach Javy znajdziemy wiele innych gotowych metod, które pozwalają podawać jako argumenty lambda-wyrażenia.

Page 37: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

37

Przetwarzanie strumieniowe

Strumień (nie należy mylić tego pojęcia ze strumieniami wejścia-wyjścia) stanowi sekwencję elementów, ale nie przechowuje tych elementów w strukturach danych, tylko konstytuuje potok (połączenie) różnych operacji na elementach. Potok operacji ma swoje źródło (można to nazwać też źródłem strumienia), który może być np. kolekcją, tablicą lub napisem (ciągiem znaków). Operacje, łączone w potoku, mogą być

● pośrednie - dające w wyniku inny strumień (np. map, filter), przy czym operacje te mogą być

– bezstanowe - niewymagające od strumienia znajomości wartości poprzednio przetwarzanych elementów (np. filter lub forEach); takie operacje są bardzo efektywne;

– stanowe - wymagające od strumienia pamiętania stanów innych, od akurat przetwarzanego, elementów (np. sortowanie elementów strumienia);

● terminalne - kończące przetwarzanie strumienia (np. collect i forEach); po wykonaniu operacji terminalnej nie można wykonać na strumieniu żadnej innej operacji;

● skracające (short-circuit) - takie, które powodują, że wcześniejsze w potoku operacje pośrednie kończą działanie, gdy rezultat operacji skracającej jest jasny; operacje te mogą być zarówno pośrednie, jak i terminalne.

Page 38: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

38

Przetwarzanie strumieniowe

Strumienie charakteryzują się ważnymi cechami.

● Operacje pośrednie są leniwe (lazy operations), czyli nie są wykonywane ani nie generują żadnych wartości, dopóki nie zostanie wywołana operacja terminalna, a elementy strumienia są przetwarzane tylko w takim zakresie, jaki jest potrzebny do uzyskania wymaganego wyniku (np. gdy rezultat operacji skracającej jest w pełni określony).

● Elementy strumienia mogą być generowane ad hoc za pomocą generatorów i iteratorów (ogólnie takie strumienie są nieskończone; w praktyce zawsze jakoś kończymy ich przetwarzanie, np. generator liczb pseudolosowych może generować nieskończoną ich sekwencję, a my ograniczamy ją do pierwszych 100).

● Operacje na strumieniach mogą być wykonywane równolegle (w odrębnych wątkach), przy czym bardzo łatwo to możemy zadekretować (np. za pomocą metody parallel), a podziałem pracy między równolegle wykonujące się podzadania zajmie się JVM.

● We wszystkich operacjach strumieniowych podajemy lambda-wyrażenia, co sprzyja zwięzłości i czytelności kodu.

Page 39: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

39

Przetwarzanie strumieniowe

Strumienie są obiektami klasy implementującej interfejs Stream. Strumienie możemy uzyskać m.in.:

● od kolekcji - za pomocą metody stream() z interfejsu Collection, np. list. stream();

● od tablic-za pomocą statycznej metody stream() z klasy Arrays, np. Arrays. stream(array);

● od napisów (strumień znaków) - za pomocą metody chars();● od podanych wartości, w tym tablic obiektowych (varargs), za pomocą metody

Stream.of(zestaw wartości);● od plików (strumień wierszy pliku) - za pomocą statycznej metody lines(...) z klasy

Files lub lines() z klasy BuferredReader;● od katalogów (strumień reprezentujący drzewo katalogowe obiektów plikowych) - za pomocą

statycznej metody walk (...) lub find(...) z klasy Files;● od archiwów (np. ZipFile lub JarFile; tu strumień reprezentuje uporządkowane elementy

archiwum entries) za pomocą metody stream();● od wyrażenia regularnego, reprezentowanego przez obiekt klasy Pattern - za pomocą

metody splitAsStream(napis), zwracającej strumień symboli napisu, wyłuskanych za pomocą tego wyrażenia;

● za pomocą generowania elementów metodami Stream.generate(...) lub Stream.iterate(... );

● za pomocą statycznej metody ints() klasy Random (strumień pseudolosowych liczb całkowitych;

● przez leniwe połączenie strumieni statyczną metodą Stream.concat(strum1, strum2).

Page 40: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

40

Przetwarzanie strumieniowe

Podstawowe operacje pośrednie:● map(Function f) - Zwraca strumień z elementami tego strumienia

przekształconymi za pomocą podanej funkcji (transformacja elementów).● flatMap(Function f) - Zwraca strumień z elementami tego strumienia

przekształconymi za pomocą podanej funkcji, przy czym zastosowanie tej funkcji musi dawać strumienie, a po odwzorowaniu ich elementy będą stanowić jeden strumień (nastąpi swoiste spłaszczenie, połączenie różnych zestawów danych w jeden).

● filter(Predicate p) - Zwraca strumień elementów, dla których predykat p daje true (selekcja elementów).

● distinct() - Zwraca strumień elementów, różniących się w sensie equals() (wybór niepowtarzających się elementów) - operacja stanowa.

● sorted(...) - Zwraca strumień posortowanych elementów (w porządku naturalnym lub wg podanego komparatora) - operacja stanowa.

● unordered() - Zwraca nieuporządkowany strumień.● limit(int n) - Zwraca strumień zawierający n pierwszych elementów

tego strumienia - operacja skracająca.● generate(Supplier s) - Zwraca strumień elementów, z których każdy

jest tworzony przez podanego „dostawcę", np. strumień stałych lub losowych wartości.

Page 41: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

41

Przetwarzanie strumieniowe

● iterate(T init, UnaryOperator op) - Zwraca strumień tworzony przez iteracyjne zastosowanie operatora op wobec inicjalnej wartości init.

● substream(...) - Zwraca część strumienia (np. od 10 elementu do końca lub od 5 do 20) - operacja skracająca.

● parallel () - Zwraca strumień, na którym operacje będą wykonywane równolegle.● sequential() - Zwraca strumień, na którym operacje będą wykonywane

sekwencyjnie.Wybrane operacje terminalne:

● allMatch(Predicate p) - Zwraca true, jeśli wszystkie elementy strumienia spełniają warunek p, false w przeciwnym razie.

● anyMatch(Predicate p) - J.w., ale pytanie brzmi: czy jakikolwiek element strumienia spełnia warunek; jest to operacja skracająca.

● noneMatch(Predicate p) - J.w, czy żaden element strumienia nie spełnia podanego warunku - operacja skracająca.

● findAny() - Zwraca dowolny element strumienia jako obiekt typu Optional, zawierający wartość tego elementu lub wartość pustą, jeśli strumień jest pusty - operacja skracająca.

● findFirst() - Zwraca pierwszy element strumienia (jako Optional z jego wartością lub wartością pustą, gdy strumień jest pusty) - operacja skracająca.

● long count() - Zwraca liczbę elementów strumienia.

Page 42: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

42

Przetwarzanie strumieniowe

● void forEach(Consumer c) - Wykonuje podane działanie na każdym elemencie strumienia; ale kolejność przetwarzania nie musi być taka sama, jak kolejność w zestawie, na który nałożono strumień (np. elementy listy mają swoją kolejność, ale forEach na strumieniu związanym z listą może wykonywać działania w innej kolejności).

● forEachOrdered(Consumer) - j.w., ale jeśli dane, od których uzyskano strumień, mają swoją kolejność, to przy przetwarzaniu zostanie ona zachowana.

● reduce(...) - Wykonuje redukcję elementów strumienia, czyli ogólnie - na podstawie zestawu jego elementów, stosując wobec nich odpowiednie operacje akumulujące czy kombinujące, wytwarza pewną wynikową wartość (np. sumę).

● collect(...) - Wykonuje tzw. redukcję modyfikowalną, akumulującą elementy strumienia do modyfikowalnego kontenera, takiego jak kolekcja, mapa, bufor znakowy, inny strumień.

● max(...) i min(…) - Zwracają największy (najmniejszy) element strumienia w porządku określonym przez podany jako argument komparator.

● toArray(…) - Zwraca tablicę elementów strumienia.

Page 43: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

43

Przetwarzanie strumieniowe

Możemy teraz zająć się przykładowymi zastosowaniami. Będziemy przy tym używać klasy opisującej państwa.

Załóżmy, że mamy plik z informacjami o krajach (zaczerpnięty z serwisu GeoNames, gdzie wymienione są nie tylko niepodległe państwa, ale również regiony, które mają odrębne kody ISO2; w informacjach tych nie ma gęstości zaludnienia, a kontynenty są dane tylko w postaci kodów). Na wiersze tego pliku możemy nałożyć strumień i łatwo je przetwarzać na różne sposoby.

Utwórzmy najpierw listę obiektów klasy Country. Zastosujemy metody map i collect strumieni do tworzenia listy obiektów klasy na podstawie wierszy pliku:

Path p = Paths.get("nazwa_pliku");Stream<String> ls = Files.lines(p, Charset.defaultCharset()); List<Country> clist = ls.map(Country::new).collect(Collectors.toList());

Tutaj map(Country::new) tworzy strumień obiektów klasy Country (na wierszach pliku jest wywoływany jej konstruktor, który inicjuje pola na podstawie informacji ze stringa), a metoda collect posługuje się predefiniowanym obiektem-kolektorem (statyczne odwołanie do klasy Collectors), który dodaje elementy strumienia do nowo utworzonej listy. Klasa Collectors dostarcza wielu innych gotowych kolektorów do wykorzystania w metodzie collect (np. do tworzenia różnych rodzajów kolekcji, a także grupowania elementów strumienia pod kluczami w mapach).

Przetwarzanie strumieniowe

Page 44: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

44

Przetwarzanie strumieniowe

Od utworzonej listy krajów możemy pobrać strumień i wykonywać na nim inne operacje, np. przezclist.stream().count()

dowiemy się, że mamy informacje o 250 krajach. Spróbujmy teraz odnaleźć pierwszy kraj w strumieniu, którego nazwa zaczyna się na „B" i pobrać jego nazwę.Optional<String> first = clist.stream() .map(Country::getName) // strumień wszystkich nazw .filter(s -> s.startsWith("B")) //strumień nazw na B .findFirst(); // pierwsza nazwa na BString nazwa = first.get(); System.out.println(nazwa);

Jako wynik findFirst() mamy Optional, bo w strumieniu może nie być żadnego elementu. Optional opakowuje uzyskaną wartość, musimy ją jeszcze pobrać za pomocą metody get(). Jednak jeśli jej nie ma (np. w filter wyszukujemy kraje o nazwie zaczynającej się na X), to wywołanie get() spowoduje wyjątek.

Page 45: Elementy programowania funkcyjnego - KUL · 2015. 1. 26. · 2 Programowanie funkcyjne - wprowadzenie Języki programowania takie jak C/C++/Java/Python są nazywane imperatywnymi

45

Przetwarzanie strumieniowe

Aby tego uniknąć, powinniśmy zastosować następująca konstrukcję:

Optional<String> first ... String nazwa = first.orElse(...)

której rezultatem będzie wynik get() (jeśli jest), albo podany przez nas argument metody orElse. Dobrze to widać w następującym fragmencie :

static String firstWithPrefix(Stream<Country> str, String p){ return str.map(Country::getName) .filter(s -> s.startsWith(p)) .findFirst().orElse("nie ma kraju na " + p); }// ...List<Country> clist = ...String nazwa = firstWithPrefix(clist.stream(), "B"); System.out.println(nazwa);nazwa = firstWithPrefix(clist.stream(), "X"); System.out.println(nazwa);

Fragment ten wyprowadzi:

Bośnia and Herzegovinanie ma kraju na X