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

14 luty

Co to jest Docker i dlaczego warto go używać?

Co to jest Docker i jak działa? Jakie korzyści niesie Docker dla programistów, testerów, administratorów i architektów IT? Przeczytaj...

Ikona kalendarza

18 styczeń

Jak się przygotować do zdania ISTQB® Foundation Level?

Jak przygotować się do zdania egzaminu ISTQB® Foundation Level? Sprawdź cenne wskazówki przed przystąpieniem do egzaminu.

Ikona kalendarza

11 styczeń

Dofinansowanie KFS na szkolenie w 2024 roku

Z artykułu dowiesz się, jak wygląda budżet KFS w 2024 roku, poznasz priorytety wydatkowania oraz dowiesz się, jak wnioskować o dofina...