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

By 9 September 2015 June 2nd, 2019 Niezbędnik Juniora

Wyraże­nia lamb­da są bard­zo przy­dat­ną zmi­aną jaka weszła do Javy 8. W przy­pad­ku dat mieliśmy do czynienia z nowym stan­dar­d­em, który zastępu­je wcześniej stosowane rozwiąza­nia (np. bib­liotekę Joda). Tym razem, nadal może­my pro­gramować “po stare­mu”, jed­nak czys­tość kodu wynika­ją­ca z praw­idłowego stosowa­nia lambd jest naprawdę fajnym zyskiem. Dlat­ego zachę­camy do zapoz­na­nia się z tym ele­mentem języ­ka i wcie­le­niem go do swo­jego codzi­en­nego programowania.

Czym są lambdy?

Jak wiecie, Java to obiek­towy język pro­gramowa­nia, jed­nak lamb­dy pozwala­ją na pisanie kodu w sposób funkcyjny. Oznacza to, że zami­ast oper­ować na stanach obiek­tów może­my bezpośred­nio deklarować, co chce­my zro­bić. Odnosząc lamb­dy do pro­gramowa­nia obiek­towego, może­my o nich myśleć jak o klasach tym­cza­sowych zaw­ier­a­ją­cych jed­ną metodę. Lamb­dy to obiek­ty, zaw­ier­a­jące frag­ment kodu: funkcję, a także specy­ficzne atry­bu­ty i para­me­try ważne dla nich (środowisko, w ramach którego ope­ru­je funkc­ja).  Może brz­mi to skom­p­likowanie, ale mam nadzieję, że poniższe przykłady ułatwią Ci zrozu­mie­nie lambd.

Przykład

Zaczniemy od przykładu bez uży­cia lambd, żebyś miała porów­nanie i łat­wo mogła zrozu­mieć, po co i jak to robimy. W kodzie pomi­jamy importy i pack­age, bo nie są one istotne, chce­my byś przede wszys­tkim skupiła się na sednie ;)

W naszym zada­niu chce­my wyp­isać listę cele­bry­tów, na pod­staw­ie ich umiejęt­noś­ci (w naszym pro­gramie cele­bryci je mają, choć nie zawsze pokry­wa się to z rzeczy­wis­toś­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 Celebri­ty ma cztery pola, które są też w jego kon­struk­torze. Ma też trzy metody, które zwraca­ją infor­ma­cję (stan) na tem­at konkret­nych umiejęt­noś­ci cele­bry­ty. Ma także metodę to String(), po to byśmy mogli łat­wo ziden­ty­fikować Cele­bry­tę w programie.

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

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

Oraz imple­men­tac­je do niego, chce­my sprawdz­ić, czy Cele­bry­ta umie śpiewać:

public class CheckIfSinger implements CheckTalent {

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

Mamy już wszys­tko, by sprawdz­ić, czy Cele­bryci 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();
        }
    }
}

Meto­da print jest dość gen­erycz­na — może sprawdz­ić każdy tal­ent, co jest dobrą prak­tyką. Co musimy więc zro­bić, by sprawdz­ić, czy Cele­bry­ta tańczy? Musimy napisać nową klasę Check­If­Dancer, a następ­nie dodać lin­ijkę w PudelekRead­ing  print(celebrities, new CheckIfDancer()) ... 

No dobra, pora na lamb­dy. Naszą lin­ijkę print(celebrities, new Check­If­Singer()) w kodzie może­my zamienić takim oto wyraże­niem (na razie nie prze­j­muj się tym, że dzi­wnie wygląda):

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

Pokazu­je­my Javie, że sku­pi­amy się na cele­bry­tach, którzy potrafią śpiewać. Jak wyglą­dało by sprawdze­nie, czy potrafią tańczyć?

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

OK, jak może zauważyłaś, by to zro­bić nie potrze­bu­je­my już klasy imple­men­tu­jącej nasz inter­fe­js, po pros­tu doda­je­my lambdę.

Sama potem zobaczysz, jak łat­wo jest pisać lamb­dy i jak bard­zo zwięk­sza­ją one czytel­ność Two­jego kodu. Wyraże­nia lamb­da wyko­rzys­tu­ją kon­cept, który nazy­wa się deferred exe­cu­tion, czyli takie, które są wykony­wanie z odrocze­niem. W naszym przy­pad­ku, to odrocze­nie to moment, gdy meto­da 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 Celebri­ty jako para­me­trem i zwró­cić booleana jako wynik c.canSing().Poniżej zna­jdziesz grafikę z dwoma doz­wolony­mi zapisa­mi wyrażeń lamb­da. Oba robią dokład­nie to samo.

skladnia lambda

Pier­wszy skła­da się z 3 częś­ci: para­metru c, strza­ł­ki która odd­ziela para­metr od ciała i ciała, które wywołu­je poje­dynczą metodę i zwraca jej wynik.

Dru­gi zapis również skła­da się z trzech częś­ci: para­metru c typu Celebri­ty zawartego w naw­iasach, strza­ł­ki, która odd­ziela para­metr od ciała i ciała, które skła­da się z jed­nej lub kilku lin­i­jek kodu, i które zaw­iera w sobie return i średnik.

Naw­iasy może­my opuś­cić tylko dla jed­nego para­metru, które­mu nie określamy wyraźnie typu. Klamer­ki mogą być pominięte tylko jeśli mamy poje­dynczą lin­ię kodu. Lamb­da może mieć też zero argu­men­tów, co zapisu­je­my po pros­tu w postaci ().

To co jeszcze warto wiedzieć o lamb­dach, to to, że mają one dostęp do zmi­en­nych i np.:

boolean wantSing = true;

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

to całkowicie poprawny kod. Warto pamię­tać, że wyraże­nia lamb­da mają dostęp do pól klasy oraz staty­cznych zmi­en­nych. Jeśli chodzi o para­me­try metody i zmi­enne lokalne, to ten dostęp jest możli­wy tylko wtedy, gdy nie przyp­isu­je­my 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 inter­fe­js check­Tal­ent z metodą test? Java zapew­nia nam taki inter­fe­js w pakiecie java.util.function:

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

Jak widzisz, wyglą­da on tak jak nasz inter­fe­js z przykładu z małą różnicą, mamy w nim typ T zami­ast Celebri­ty. W taki sposób oznacza­my gen­ery­ki (które dzi­ała­ją jak szablony klas/interfejsów, bo konkret­ny para­metr poda­je­my im dopiero w momen­cie uży­cia). Jeśli więc wrócimy do naszego kodu to korzys­ta­jąc z tego inter­fe­j­su, klasa PudelekRead­ing 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();
        }
    }
}

Dos­zliśmy więc do tego momen­tu — z czterech klas, zro­biły się dwie, przy czym sam zapis z uży­ciem lamb­dy, gdy już go rozu­miemy, jest nawet czytel­niejszy. Koniec z teorią, pora na kil­ka przykładów zas­tosowa­nia 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 pojaw­ił się nowy oper­a­tor (::), który przek­sz­tał­ca metodę w wyraże­nie lamb­da, więcej o takim zapisie przeczy­tasz 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 stosowa­nia lambd, ale zan­im go przed­staw­imy wytłu­maczymy czym są stru­mie­nie w Javie 8.
Stru­mień (Steam) reprezen­tu­je sek­wenc­je ele­men­tów i pozwala na różne oper­ac­je na tych ele­men­tach. Oper­ac­je te mogą być pośred­nie i takie może­my układać w łańcuchy metod, oraz koń­cowe, zwraca­jące wynik, lub nie.

Poniżej przykład oper­acji na strumieniu:


List fruits =
    Arrays.asList("apple", "banana", "cherry", "plum", "pear", "pinapple");
fruits.stream().filter(s -> s.toString().startsWith("p")).map(s -> s.toString().toUpperCase()).sorted().forEach(System.out::println);
Jaki będzie wynik tego łańcuchu operacji?

PEAR
PINAPPLE
PLUM

Przeanal­izu­jmy więc, co się stało.
Kulisy branży - cytaty-2
stream() — zamienia listę na stru­mień, fil­ter(…) — w naszym przy­pad­ku fil­tru­je te owoce, które zaczy­na­ją się na p, map(…) — zamienia każdy string na wielkie litery, sort­ed() — sor­tu­je AZ, a forE­ach(…) — wyp­isu­je je w nowych lini­ach. filter(),map(),sorted() są pośred­nie, nato­mi­ast forE­ach() koń­cowa i zwraca nam ona wynik. Po pełną listę oper­acji jakie może­my wykony­wać na stru­mieni­ach odsyłamy do doku­men­tacji, warto zwró­cić uwagę na oper­ac­je col­lect, która pozwala na zamie­nie­nie stru­mienia ele­men­tów w kolekc­je np. listę, stos czy mapę.

Więk­szość oper­acji na stru­mieni­ach może przyj­mować jako swo­je para­me­try pewne wyraże­nia lamb­da, ale też takie oper­ac­je muszą speł­ni­ać pewne warun­ki: muszą być niein­ter­fer­u­jce (non-inter­fer­ing) i bezs­tanowe (state­less).

Funkc­ja jest niein­ter­fer­u­ja­ca, gdy nie mody­fiku­je pod­sta­wowego źródła danych do stru­mienia (nie usuwa, nie edy­tu­je, nie zmienia ele­men­tów ze stru­mieniowanej kolekcji).

Funkc­ja jest bezs­tanowa, gdy wynik oper­acji jest deter­min­isty­czny, czyli nie zależy od żad­nej zmi­en­nej, którą moż­na zmieni­ać (która jest muta­ble) albo stanu z zewnętrznego źródła, który może się zmieni­ać w trak­cie jej wykonywania.

Aby korzys­tać ze stru­mienia nie musimy wcale tworzyć najpierw kolekcji, wystar­czy wtedy wyko­rzys­tać Stream.of(). Oprócz stru­mieni, które przyj­mu­ją obiek­ty, ist­nieją też IntStream, LongStream, Dou­bleStream, które przyj­mu­ją kole­jne prymi­ty­wy: int, long, dou­ble. Dzię­ki mapowa­niu może­my zamieni­ać stru­mie­nie obiek­towe na te z prymi­ty­wów i odwrotnie.

Prze­jdźmy do bardziej złożonego przykładu, w którym pokaże­my Ci zas­tosowanie wyrażeń lambda.

 public class Beverage{
String name;
int price;
Beverage
(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));

 

Zaczni­jmy od prostego filtru:


List<Beverage> filtered = beverages
    .stream()
    .filter(b -> b.name.startsWith("C"))
    .collect(Collectors.toList());

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

Za pomocą wyraże­nia lamb­da stworzyliśmy fil­tr, fil­tru­ją­cy napo­je. których nazwa zaczy­na się na C, a następ­nie zapisal­iśmy wynik tej oper­acji jako listę, za pomocą oper­acji collect.

Ter­az spróbu­je­my pogrupować nasze napo­je 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 napo­jów, które tyle kosz­tu­ją. Nie prze­j­muj się skład­nią argu­men­tu System.out.format(), użyliśmy go tutaj tylko po to by pokazać Ci to pogrupowanie — dociek­li­wi zna­jdą więcej info tutaj.

Kole­jnym zadaniem będzie oblicze­nie śred­niej ceny za napój:

 


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

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

A ter­az, wyp­isze­my wszys­tkie napo­je, na które jest dodatkowa pro­moc­ja, ustalmy, że to te kosz­tu­ją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ład­nia join­ing() to 3 opcjon­alne ele­men­ty: ele­ment odd­ziela­ją­cy, pre­fix i sufix.

To solidne pod­stawy do pra­cy ze stru­mieni­a­mi i wyraże­ni­a­mi lamb­da. Po więcej odsyłamy do lit­er­atu­ry dodatkowej. Mamy nadzieję, że dzię­ki tej lekcji zaczniesz uży­wać lambd, które naprawdę ułatwia­ją pracę programisty.