#Niezbędnik Juniora. Wyrażenia lambda i strumienie

By 9 września 2015Niezbędnik Juniora

Wyrażenia lambda są bardzo przydatną zmianą jaka weszła do Javy 8. W przypadku dat mieliśmy do czynienia z nowym standardem, który zastępuje wcześniej stosowane rozwiązania (np. bibliotekę Joda). Tym razem, nadal możemy programować „po staremu”, jednak czystość kodu wynikająca z prawidłowego stosowania lambd jest naprawdę fajnym zyskiem. Dlatego zachęcamy do zapoznania się z tym elementem języka i wcieleniem go do swojego codziennego programowania.

Czym są lambdy?

Jak wiecie, Java to obiektowy język programowania, jednak lambdy pozwalają na pisanie kodu w sposób funkcyjny. Oznacza to, że zamiast operować na stanach obiektów możemy bezpośrednio deklarować, co chcemy zrobić. Odnosząc lambdy do programowania obiektowego, możemy o nich myśleć jak o klasach tymczasowych zawierających jedną metodę. Lambdy to obiekty, zawierające fragment kodu: funkcję, a także specyficzne atrybuty i parametry ważne dla nich (środowisko, w ramach którego operuje funkcja).  Może brzmi to skomplikowanie, ale mam nadzieję, że poniższe przykłady ułatwią Ci zrozumienie lambd.

Przykład

Zaczniemy od przykładu bez użycia lambd, żebyś miała porównanie i łatwo mogła zrozumieć, po co i jak to robimy. W kodzie pomijamy importy i package, bo nie są one istotne, chcemy byś przede wszystkim skupiła się na sednie ;)

W naszym zadaniu chcemy wypisać listę celebrytów, na podstawie ich umiejętności (w naszym programie celebryci je mają, choć nie zawsze pokrywa się to z rzeczywistością ;)). Mamy więc klasę Celebrity:

public class Celebrity {
    private String name;
    private boolean canSing;
    private boolean canAct;
    private boolean canDance;

    public Celebrity(String starName, boolean singer, boolean actor, boolean dancer) {
        this.name = starName;
        this.canSing = singer;
        this.canAct = actor;
        this.canDance = dancer;
    }

    public boolean canSing() {
        return canSing;
    }

    public boolean canDance() {
        return canDance;
    }

    public boolean canAct() {
        return canAct;
    }

    public String getName() {
        return name;
    }

    public String toString() {
        return getName();
    }
}

Obiekt Celebrity ma cztery pola, które są też w jego konstruktorze. Ma też trzy metody, które zwracają informację (stan) na temat konkretnych umiejętności celebryty. Ma także metodę to String(), po to byśmy mogli łatwo zidentyfikować Celebrytę w programie.

Jako, że będziemy wykonywać wiele różnych sprawdzeń, tworzymy interfejs:

public interface CheckTalent{
    boolean test (Celebrity celebrity);
}

Oraz implementacje do niego, chcemy sprawdzić, czy Celebryta umie śpiewać:

public class CheckIfSinger implements CheckTalent {

    boolean test (Celebrity celebrity) {
        return celebrity.canSing();
    }
}

Mamy już wszystko, by sprawdzić, czy Celebryci potrafią śpiewać.

public class PudelekReading {
    public static void main (String[] args){
        List<Celebrity> celebrities = new ArrayList<Celebrity>(); //lista celebrytów
        celebrities.add(new Celebrity("Justin Bieber", true, false, true));
        celebrities.add(new Celebrity("Kim Kardashian", false, false, false));
        celebrities.add(new Celebrity("Joanna Krupa", true, true, false));

        print(celebrities, new CheckIfSinger());//przekazujemy klasę, która sprawdza
    }
    private static void print(List<Celebrity> celebrities, CheckTalent checker){
        for (Celebrity celebrity:celebrities){
            if(checker.test(celebrity)){
                System.out.println(celebrity + " ");
            }
            System.out.println();
        }
    }
}

Metoda print jest dość generyczna – może sprawdzić każdy talent, co jest dobrą praktyką. Co musimy więc zrobić, by sprawdzić, czy Celebryta tańczy? Musimy napisać nową klasę CheckIfDancer, a następnie dodać linijkę w PudelekReading  print(celebrities, new CheckIfDancer()) ... 

No dobra, pora na lambdy. Naszą linijkę print(celebrities, new CheckIfSinger()) w kodzie możemy zamienić takim oto wyrażeniem (na razie nie przejmuj się tym, że dziwnie wygląda):

print(celebrities,c -> c.canSing());

Pokazujemy Javie, że skupiamy się na celebrytach, którzy potrafią śpiewać. Jak wyglądało by sprawdzenie, czy potrafią tańczyć?

 print(celebrities,c -> c.canDance()); 

OK, jak może zauważyłaś, by to zrobić nie potrzebujemy już klasy implementującej nasz interfejs, po prostu dodajemy lambdę.

Sama potem zobaczysz, jak łatwo jest pisać lambdy i jak bardzo zwiększają one czytelność Twojego kodu. Wyrażenia lambda wykorzystują koncept, który nazywa się deferred execution, czyli takie, które są wykonywanie z odroczeniem. W naszym przypadku, to odroczenie to moment, gdy metoda print() jest wywołana.

Pisanie prostych wyrażeń lambda

Wróćmy do naszego przykładu:

c -> c.canSing();

Taki zapis mówi Javie, że ma wywołać metodę z Celebrity jako parametrem i zwrócić booleana jako wynik c.canSing().Poniżej znajdziesz grafikę z dwoma dozwolonymi zapisami wyrażeń lambda. Oba robią dokładnie to samo.

skladnia lambda

Pierwszy składa się z 3 części: parametru c, strzałki która oddziela parametr od ciała i ciała, które wywołuje pojedynczą metodę i zwraca jej wynik.

Drugi zapis również składa się z trzech części: parametru c typu Celebrity zawartego w nawiasach, strzałki, która oddziela parametr od ciała i ciała, które składa się z jednej lub kilku linijek kodu, i które zawiera w sobie return i średnik.

Nawiasy możemy opuścić tylko dla jednego parametru, któremu nie określamy wyraźnie typu. Klamerki mogą być pominięte tylko jeśli mamy pojedynczą linię kodu. Lambda może mieć też zero argumentów, co zapisujemy po prostu w postaci ().

To co jeszcze warto wiedzieć o lambdach, to to, że mają one dostęp do zmiennych i np.:

boolean wantSing = true;

print(celebrities, c ->c.canSing()==wantSing);

to całkowicie poprawny kod. Warto pamiętać, że wyrażenia lambda mają dostęp do pól klasy oraz statycznych zmiennych. Jeśli chodzi o parametry metody i zmienne lokalne, to ten dostęp jest możliwy tylko wtedy, gdy nie przypisujemy do nich nowych wartości. Czyli:

(a,b) -> {int a=0;return 5;} // kod się nie skompiluje
(a,b) -> {int c=0;return 5;} // to jest ok

Interfejs Predictate<T>

Wróćmy jeszcze na chwilę do naszego przykładu, pamiętasz interfejs checkTalent z metodą test? Java zapewnia nam taki interfejs w pakiecie java.util.function:

public interface Predicate{
    boolean test (T t);
}

Jak widzisz, wygląda on tak jak nasz interfejs z przykładu z małą różnicą, mamy w nim typ T zamiast Celebrity. W taki sposób oznaczamy generyki (które działają jak szablony klas/interfejsów, bo konkretny parametr podajemy im dopiero w momencie użycia). Jeśli więc wrócimy do naszego kodu to korzystając z tego interfejsu, klasa PudelekReading wygląda następująco:


public class PudelekReading {
    public static void main (String[] args){
        List celebrities = new ArrayList(); //lista celebrytów
        celebrities.add(new Celebrity("Justin Bieber", true, false, true);
        celebrities.add(new Celebrity("Kim Kardashian", false, false, false);
        celebrities.add(new Celebrity("Joanna Krupa", true, true, false);
        print(celebrities,c -> c.canSing());//wyrażenie lambda
    }
    private static void print(List celebrities, Predicate<Celebrity> checker){ // interfejs Predicate
        for (Celebrity celebrity : celebrities){
            if(checker.test(celebrity)){
                System.out.println(celebrity + " ");
            }
            System.out.println();
        }
    }
}

Doszliśmy więc do tego momentu – z czterech klas, zrobiły się dwie, przy czym sam zapis z użyciem lambdy, gdy już go rozumiemy, jest nawet czytelniejszy. Koniec z teorią, pora na kilka przykładów zastosowania lambd.

Lambda zamiast pętli


String[] a = {"cat", "dog", "mouse", "rat", "pig", "rabbit", "hamster", "parrot"};
List animals =  Arrays.asList(a);

// Tradycyjna pętla
for (String animal : animals) {
     System.out.print(animal + "; ");
}

//  Wyrażenie lambda
animals.forEach((animal) -> System.out.print(animal + "; "));

// Z użyciem podwójnego dwukropka
animals.forEach(System.out::println);

W powyższym przykładzie pojawił się nowy operator (::), który przekształca metodę w wyrażenie lambda, więcej o takim zapisie przeczytasz tutaj.

Tworzenie nowego wątku


 //podejście klasyczne
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello world!");
    }
}).start();

//wyrażenie lambda
new Thread(() -> System.out.println("Hello world!")).start();

Sortowanie kolekcji


String[] animals = {"cat", "dog", "mouse", "rat", "pig", "rabbit", "hamster", "parrot"};

// sortowanie zwierząt z kolekcji z użyciem anonimowej klasy wewnętrznej 
Arrays.sort(animals, new Comparator<String>() {
	@Override
	public int compare(String s1, String s2) {
		return (s1.compareTo(s2));
	}
});

//sortowanie zwierząt z kolekcji z użyciem lambd
Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2));
Arrays.sort(animals, sortByName);

// albo
Arrays.sort(animals, (String s1, String s2) -> (s1.compareTo(s2)));

Praca ze strumieniami

Tutaj pora na naprawdę super użyteczny przykład stosowania lambd, ale zanim go przedstawimy wytłumaczymy czym są strumienie w Javie 8.
Strumień (Steam) reprezentuje sekwencje elementów i pozwala na różne operacje na tych elementach. Operacje te mogą być pośrednie i takie możemy układać w łańcuchy metod, oraz końcowe, zwracające wynik, lub nie.

Poniżej przykład operacji na strumieniu:


List fruits =
    Arrays.asList("apple", "banana", "cherry", "plum", "pear", "pinapple");

myList
    .stream()
    .filter(s -> s.startsWith("p"))
    .map(String::toUpperCase)
    .sorted()
    .forEach(System.out::println);

Jaki będzie wynik tego łańcuchu operacji?


PEAR
PINAPPLE
PLUM

Przeanalizujmy więc, co się stało.
Kulisy branży - cytaty-2
stream() – zamienia listę na strumień, filter(…) – w naszym przypadku filtruje te owoce, które zaczynają się na p, map(…) – zamienia każdy string na wielkie litery, sorted() – sortuje AZ, a forEach(…) – wypisuje je w nowych liniach. filter(),map(),sorted() są pośrednie, natomiast forEach() końcowa i zwraca nam ona wynik. Po pełną listę operacji jakie możemy wykonywać na strumieniach odsyłamy do dokumentacji, warto zwrócić uwagę na operacje collect, która pozwala na zamienienie strumienia elementów w kolekcje np. listę, stos czy mapę.

Większość operacji na strumieniach może przyjmować jako swoje parametry pewne wyrażenia lambda, ale też takie operacje muszą spełniać pewne warunki: muszą być nieinterferujce (non-interfering) i bezstanowe (stateless).

Funkcja jest nieinterferujaca, gdy nie modyfikuje podstawowego źródła danych do strumienia (nie usuwa, nie edytuje, nie zmienia elementów ze strumieniowanej kolekcji).

Funkcja jest bezstanowa, gdy wynik operacji jest deterministyczny, czyli nie zależy od żadnej zmiennej, którą można zmieniać (która jest mutable) albo stanu z zewnętrznego źródła, który może się zmieniać w trakcie jej wykonywania.

Aby korzystać ze strumienia nie musimy wcale tworzyć najpierw kolekcji, wystarczy wtedy wykorzystać Stream.of(). Oprócz strumieni, które przyjmują obiekty, istnieją też IntStream, LongStream, DoubleStream, które przyjmują kolejne prymitywy: int, long, double. Dzięki mapowaniu możemy zamieniać strumienie obiektowe na te z prymitywów i odwrotnie.

Przejdźmy do bardziej złożonego przykładu, w którym pokażemy Ci zastosowanie wyrażeń lambda.

 public class Beverage{
String name;
int price;

Person (String name, int price){
this.name = name;
this.age = price;
    }
@Override 
public String toString(){
return name;
    }
}
Tworzymy też listę napojów

List beverage = Arrays.asList(
new Beverage("Cola", 2),
new Beverage("HipsterCola", 5),
new Beverage("SuperHipsterCola", 5),
new Beverage("UltraSuperHipsterCola", 10),
new Beverage("CheapCola", 2));

 

Zacznijmy od prostego filtru:


List filtred =
beverages
    .stream()
    .filter(b->b.name.startsWith("C"))
    .collect(Collector.toList());

System.out.println(filtred); // [Cola, CheapCola]

Za pomocą wyrażenia lambda stworzyliśmy filtr, filtrujący napoje. których nazwa zaczyna się na C, a następnie zapisaliśmy wynik tej operacji jako listę, za pomocą operacji collect.

Teraz spróbujemy pogrupować nasze napoje według ceny:


Map<Integer, List> colaByPrice = 
beverages
    .stream()
    .collect(Collectors.groupingBy(b -> b.price));
colaByPrice
    .forEach((price,p) -> System.out.format("price %s: %s\n",price, p));
//price 2: [Cola, CheapCola]
//price 5: [HipsterCola, SuperHipsterCola]
//price 10: [UltraSuperHipsterCola]

Tym razem, stworzyliśmy mapę, której kluczem jest cena, a wartością lista napojów, które tyle kosztują. Nie przejmuj się składnią argumentu System.out.format(), użyliśmy go tutaj tylko po to by pokazać Ci to pogrupowanie – dociekliwi znajdą więcej info tutaj.

Kolejnym zadaniem będzie obliczenie średniej ceny za napój:

 


Double averagePrice = 
beverages
    .stream()
    .collect(Collectors.averagingInt(b -> b.price));

System.out.println(averagePrice); //4.8

A teraz, wypiszemy wszystkie napoje, na które jest dodatkowa promocja, ustalmy, że to te kosztujące >2.


String specialOffer = 
beverages
    .stream()
    .filter(b -> b.price >2)
    .map(b -> b.name)
    .collect(Collectors.joining(" and ", "Special offer:", "are -20%.")); //

System.out.println(specialOffer); //Special Offer: HipsterCola and SuperHipsterCola and UltraSuperHipsterCola are -20%.

Składnia joining() to 3 opcjonalne elementy: element oddzielający, prefix i sufix.

To solidne podstawy do pracy ze strumieniami i wyrażeniami lambda. Po więcej odsyłamy do literatury dodatkowej. Mamy nadzieję, że dzięki tej lekcji zaczniesz używać lambd, które naprawdę ułatwiają pracę programisty.

 

  •  
  •  
  •  
  •  
  •  
  • Michal1511

    Świetny i bardzo przydatny tekst! Zaraz biorę się za jakieś proste ćwiczenia ;)
    Dziękuje i z niecierpliwością czekam na kolejne poradniki z innych rzeczy :)

    ps. Dwie małe literówki, które wpadły mi w oko:
    1. O jedno „e” za dużo w słowie „kluczem” w zdaniu – „Tym razem, stworzyliśmy mapę, której klueczem jest cena, a wartoś­cią lista napo­jów, ”
    2.Oraz brak literki „d” w słowie „lambd” na końcu tekstu – „Mamy nadzieję, że dzięki tej lekcji zaczniesz uży­wać lamb, które naprawdę ułatwiają pracę programisty.”

    Pozdrawiam!

    • Dziękujemu, już poprawione! Staramy się usuwać wszystkie literówki, ale czasem jeszcze coś umknie naszej uwadze ;)

      • Michal1511

        Choćby nie wiem ile osób wam to sprawdzało przed opublikowanie to i tak zawsze się coś znajdzie ;)
        Pozdrawiam!

        • tomaszw

          Nie kompilujecie tego kodu przed wypuszczeniem na stronkę? nie sprawdzacie czy jest poprawny?

          • Dłuższe fragmenty kodu oczywiście kompilujemy, krótsze zdarza nam się pisać ‚z głowy’ lub poprawiać już we wpisie (nazwy zmiennych itp) – niestety nie zawsze czas pozwala na bardzo dokładne sprawdzenie wszystkich fragmentów

      • Vanley

        Nie piszcie na glodniaka ;)
        nien­ter­fer­u­jaca

  • staszko032

    Końcówka tekstu:

    @Override
    public String to String(){
    return name;
    }
    }

    niepotrzebna spacja ;)
    Końcówka końcówka tekstu:

    Map colaByPrice =
    beverages
    .stream()
    .collect(Collectors.groupingBy(b -> b.price));
    beverages
    .forEach((price,p) -> System.out.format(„price %s: %sn”,price, p));

    ostatnie „beverages” nie powinno być zamienione na „colaByPrice”?

    • Jak najbardziej masz racje, już poprawione, dziękujemy!

  • LAZY MACFAG

    ‚deffed exe­cu­tion’
    No nie facet. Deferred execution. Is this ok for you?

  • michał

    Kiedy robię pierwsze przykłady bez lambd dostaję:
    Exception in thread „main” java.lang.Error: Unresolved compilation problem:
    Type mismatch: cannot convert from element type Object to Celebrity

    Jak przekonwertować te typy?

    • michał

      Okay. Resolved. Zostawiam innym pogłówkowanie, warto! Czeka na was oprócz tego jeszcze jedna pułapka! Dzięki KDK

      • Faktycznie, w jednym miejscu brakowało przy Liście – dodaliśmy, ponieważ celem przykładu nie jest konieczność główkowania (ale jednocześnie gratulacje za samodzielne rozwiązanie! :) )

  • Ola

    Cześć,
    świetny tekst :)
    Jedyne co to mi na czerwono podreślało w :

    Arrays.sort(animals, new Comparator() {

    @Override

    public int compare(String s1, String s2) {

    return (s1.compareTo(s2));

    }

    });

    //sortowanie zwierząt z kolekcji z użyciem lambd

    Comparator sortByName = (String s1, String s2) -> (s1.compareTo(s2));

    Arrays.sort(animals, sortByName);

    więc dodam przy Compartorach

    • Faktycznie umknęły nam – już poprawiamy. Dzięki za zwrócenie uwagi!

  • fisherek

    Na początku, w pierwszym przykładzie (tym bez wykorzystania lambdy) w klasie PudelekReading przy inicjalizacji listy, typ elementów listy jest błędny. Zamiast Celebrities, Celebrity

    oraz brakuje prawych nawiasów w liniach dodających obiekty do listy, np.:
    celebrities.add(new Celebrity(„Justin Bieber”, true, false, true); <–brak prawego nawiasu

    • Masz rację, wielkie dzięki za uwagi! I gratulujemy też spostrzegawczości ;)
      Kody oczywiście poprawiamy

  • bartzf

    Warto wspomnieć też, że interfejsy tj. Predicate to interfejsy funkcyjne, czyli interfejsy posiadające tylko jedną metodę. Wraz z Javą 8 doszło ich kilka, oto najpopularniejsze:
    – Predicate,
    – Function,
    – Supplier,
    – UnaryOperation,
    – Consumer.
    W Waszym kodzie w argumentach metody nie daliście ani typu generycznego ani nazwy referencji Predicate.

    Warto też jest mieć na uwadze, że lambdy mogą być dosyć wolne, za to ich możliwości są naprawdę świetne.
    Pozdrawiam, liczę że poprawicie ww. błędy.

    • Wydajnościowo lambdy nie różnią się od użycia obiektów – to od programisty zależy jak bardzo będą wydajne (lub niewydajne), nie jest to cechą lambd jako elementu języka.
      Co do interfejsów funkcyjnych, oczywiście powyższe są takimi interfejsami. Ich zastosowanie jest jednak ograniczone do specyficznych metod API Javy, opisywanie wszystkich interfejsów funkcyjnych i ich zastosowań zdecydowanie nie było celem artykułu – ich znajomość też nie za bardzo przełoży się na świadomość czym są lambdy i jak z nich korzystać w codziennej pracy, dlatego pozwolimy sobie zostawić tą listę w komentarzu :)
      Pozdrawiam, i liczę że wyjaśnisz trochę bliżej o jakie błędy konkretnie chodziło?

      • bartzf

        Hymmm, chodziło mi o te najprostsze funkcje, forEach po strumieniu może być wolniejszy od zwykłego forEacha, za to możliwości są naprawdę innowacyjne, a od programisty zależy jak bardzo chce to wykorzystać. Na pewno lepszy jest taki układ niż siedzenie na przestarzałych Javach 7*, widzę dużo takich osób.

        Dodawając komentarz miałem głównie na uwadze to, że nie było to celem poradnika, z tego względu pozwoliłem sobie na podzielenie się z tym w komentarzu, może się komuś przydać. ;>

        Co do błędów:
        1. Gdy pokazywaliście zastosowanie oraz przedstawienie interfejsu funkcyjnego Predicate, podaliście kod zapominając o dosyć istotnym generyku T.
        2. private static void print(List celebrities, Predicate){ // interfejs Predicate
        for (Celebrity celebrity:celebrities){
        if(checker.test(celebrity)){
        System.out.println(celebrity + ” „);
        }
        System.out.println();
        }
        }
        Tutaj zapomnieliście o nazwie interfejsu Predicate i o samym typie obiektu, na którym macie zamiar pracować.
        Pozdrawiam ;)

        • Ajć, faktycznie – czytałem to wczoraj z kilka razy szukając problemu i nadal nie zauważyłem :( Dzięki bardzo! Uwagi o problemie w pierwszym komentarzu nie zrozumiałem za bardzo, stąd wątpliwości ;)

          Jeszcze raz dzięki za zwrócenie uwagi!

          • bartzf

            Tak z ciekawości póki jest okazja – poradniki piszą kobiety czy mężczyźni i w jakim wieku są? Pozdrawiam i polecam się na przyszłość.

  • uzek

    Brak obrazków :(

    • poprawione, ostatnio padła strona i jak widać nie wszystko udało się odtworzyć, dzięki za sygnał!