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
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.