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

sages.pl 4 lat temu
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 nie należące do żadnej klasy, ale 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.

```
public class Person {


private String name;


private int age;


// constructors, getters and setters
```

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

```
List persons = new ArrayList();
persons.add(new Person("Andrzej", 30));
persons.add(new Person("Stefan", 26));
persons.add(new Person("Katarzyna", 29));

Collections.sort(persons, new Comparator() {
@Override
public int compare(Person p1, Person p2) {
return p1.getName().compareTo(p2.getName());
}
});
```

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:

```
p1.getName().compareTo(p2.getName());
```
Jest tu logiką biznesową. Z pomocą przychodzą lambdy:

```
Collections.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żeli 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**:

```
public class Person {

private String name;

private int age;

private List pets;

// constructors, getters and setters

}
```

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

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

```
List personsUnder30Age = persons.stream()
.filter(person -> person.getAge() person.getAge() names = persons.stream()
.map(person -> person.getName())
.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ą:

```
.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(...)**:

```
Set petNames = persons.stream()
.map(person -> person.getPets())
.flatMap(pets -> pets.stream())
.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, iż wprowadzają pewien narzut, także w przypadku bardzo rygorystycznych wymagań czasowych może się okazać, iż 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.
Idź do oryginalnego materiału