Java - lambdy i streamy, czyli o programowaniu funkcyjnym słów kilka

Rafał Pieńkowski
Ikona kalendarza
17 marca 2020

Java 8 została wypuszczona już dobrych kilka lat temu, a streamy, które wprowadziła, ułatwiają programowanie java developerom prawie każdego dnia. Zatem warto, aby się dobrze z nimi zapoznać.

Stream API używa lambd, więc zacznijmy od wyjaśnienia sobie, czym są i po co służą owe „lambdy”. W uproszczeniu mówiąc, są to anonimowe metody, czyli metody nienależące do żadnej klasy, lecz których definicję piszemy od razu w miejscu ich wywołania. Zobrazujemy sobie to za chwilę na przykładzie, jednak zanim to zrobimy, utwórzmy prostą klasę Person, która będzie nam służyła także w dalszej części artykułu.

1public class Person {
2    
3    private String name;
4    
5    private int age; 
6    
7    // constructors, getters and setters

Mamy prostą klasę Person, w której mamy pola name oraz age. Dla przejrzystości przyjmijmy, że są zdefiniowane konstruktory, gettery oraz settery. Załóżmy, że mamy listę takich osób, a następnie, chcemy tę listę posortować według imienia:

1List<Person> persons = new ArrayList<>();
2persons.add(new Person("Andrzej", 30));
3persons.add(new Person("Stefan", 26));
4persons.add(new Person("Katarzyna", 29));
5
6Collections.sort(persons, new Comparator<Person>() {
7    @Override
8    public int compare(Person p1, Person p2) {
9        return p1.getName().compareTo(p2.getName());
10    }
11});

Metoda sort jako drugi argument przyjmuje obiekt typu Comparator, więc w tym wypadku tworzymy klasę anonimową. Przed javą 8, trzeba było pisać tak jak na przykładzie powyżej – sporo „boilerplate kodu”, czyli kodu wymaganego przez kompilator, ale nie wnoszącego nic do logiki biznesowej. Tak naprawdę tylko:

1p1.getName().compareTo(p2.getName());

Jest tu logiką biznesową. Z pomocą przychodzą lambdy:

1Collections.sort(persons,(p1, p2) -> p1.getName().compareTo(p2.getName()));

Omówmy poszczególne elementy: (p1, p2) to zestaw argumentów. Jak widać, nie potrzeba definiowania typów – kompilator wie o nich. -> to symbol składniowy, oddzielający argumenty od ciała naszej lambdy. Następnie jest już ciało lambdy – jeśli można zapisać je w jednej linijce, niepotrzebne są zarówno nawiasy klamrowe jak i słówko return.

Kiedy już znamy lambdy, przejdźmy do „mięsa”, czyli Stream API. Najczęściej jest ono używane, kiedy chcemy daną kolekcję przefiltrować lub przekształcić, przy używaniu łatwiejszego i bardziej czytelnego kodu niż standardowe zagnieżdżone pętle. Rozszerzmy naszą początkową klasę Person:

1public class Person {
2
3    private String name;
4
5    private int age;
6
7    private List<String> pets;
8
9    // constructors, getters and setters
10
11}

Dołożyliśmy listę nazw zwierząt danej osoby.

Załóżmy, że mając listę takich osób, chcemy otrzymać listę takich osób, które mają mniej niż 30 lat:

1 List<Person> personsUnder30Age = persons.stream()
2                                    .filter(person -> person.getAge() < 30)
3                                    .collect(Collectors.toList());

Przeanalizujmy krok po kroku, co się dzieje:

  • .stream() powoduje powstanie obiektu typu Stream. W ten sposób budujemy stream z kolekcji.
  • .filter(...) jest metodą operującą na streamie, która filtruje jego elementy i przepuszcza dalej tylko te, które spełniają podany warunek, tzn. ciało metody zwróci true. Metoda ta przyjmuje stream i zwraca również stream. Jeśli chcielibyśmy dodać kolejną metodę operującą na streamie, moglibyśmy wywołać ją bezpośrednio po użyciu .filter(...).
  • person -> person.getAge() < 30 jest warunkiem filtrującym, zapisanym jako lambda
  • .collect(Collectors.toList()) to operacja kończąca, która zamienia wynikowy stream w daną kolekcję, w tym przypadku, w listę.
  • personsUnder30Age jest listą wynikową. Jest to zupełnie nowa lista, tzn. lista persons pozostaje bez zmian.

Załóżmy, że mając listę osób, chcemy dostać listę ich imion. Innymi słowy, chcemy w zgrabny sposób przekształcić obiekty typu Person w listę Stringów. Zrealizuje to poniższy kod:

1List<String> names = persons.stream()
2                        .map(person -> person.getName())
3                        .collect(Collectors.toList());

Widzimy tutaj podobieństwa do przykładu z filtrowaniem, jednak różnicą jest użycie metody .map(...) zamiast .filter(...). Dokonuje ona przemapowania – w tym przykładzie, przekształca ona stream elementów typu Person w stream elementów typu String (według użycia metody getName() obiektu Person).

Kolejnym przykładem, będzie sytuacja przekształcenia listy osób na listę nazw zwierząt – np. chcemy znać wszystkie dostępne zwierzęta w danej grupie osób (czyli np „pies”, „kot” itp). Na pierwszy rzut oka, moglibyśmy zacząć z następującą lambdą:

1.map(person -> person.getPets())

Jednak jak się przekonamy, zamiast streamu elementów typu String, dostaniemy stream elementów typu List! Nie o to nam chodziło, więc musimy jakoś „spłaszczyć” tę strukturę. Z pomocą przychodzi metoda .flatMap(...):

1Set<String> petNames = persons.stream()
2                        .map(person -> person.getPets())
3                        .flatMap(pets -> pets.stream())
4                        .collect(Collectors.toSet());

A więc używamy mapowania normalnie tak, jak chcemy, jednak później, jest potrzeba spłaszczenia streama – dzięki temu zbiegowi, później operujemy już na streamie Stringów. Ale uwaga – różni ludzie mogli mieć te same zwierzęta, więc aby uniknąć duplikatów, powinniśmy użyć tym razem setu zamiast listy.

Streamy w javie pozwalają na szybkie pisanie czytelnego kodu, jednak należy mieć na uwadze, że wprowadzają pewien narzut, także w przypadku bardzo rygorystycznych wymagań czasowych może się okazać, że pozostanie przy standardowych pętlach, będzie bardziej efektywne. W powyższym artykule przedstawiłem 3 najczęściej używane metody operacji na streamach, tj. .map(...), .filter(...) oraz .flatMap(...). Oczywiście Stream API jest dużo bogatsze, zatem zachęcam do jego explorowania i próbowania różnych kombinacji.

Przeczytaj także

Ikona kalendarza

29 marzec

To be or not to be a multi cloud company? Przewodnik dla kadry kierowniczej, doradców ds. chmury i architektów IT. (część 2)

Po przeczytaniu pierwszej części poradnika zauważymy, że strategie organizacji są różne. Część firm oparło swój biznes wyłącznie na j...

Ikona kalendarza

27 wrzesień

Sages wdraża system Omega-PSIR oraz System Oceny Pracowniczej w SGH

Wdrożenie Omega-PSIR i Systemu Oceny Pracowniczej w SGH. Sprawdź, jak nasze rozwiązania wspierają zarządzanie uczelnią i potencjałem ...

Ikona kalendarza

12 wrzesień

Playwright vs Cypress vs Selenium - czy warto postawić na nowe?

Playwright, Selenium czy Cypress? Odkryj kluczowe różnice i zalety każdego z tych narzędzi do automatyzacji testów aplikacji internet...