06 kwietnia 2014

O java.util.stream przez przykład(zik) z StackOverflow i Java 8 API

Istnieje wiele sposobów na poznanie nowego języka programowania i jednym z moich typów jest aktywność na StackOverflow. Można pokusić się na odpowiadanie na pytania (to stopień najwyższy wtajemniczenia), można “lajkować” (to stopień podstawowy), można edytować pytania i odpowiedzi uzupełniając je o dodatkowe informacje, które pozyskuje się z komentarzy czy wskazywać duplikaty (to stopień średniozaawansowany). Sprawdziło mi się to podczas nauki narzędzia sbt, więc kwiecień i kolejne kilka miesięcy zamierzam współdzielić z aktywnością w obszarze Java 8 (etykieta java-8 na StackOverflow).

W zależności od etykiety i dnia roboty może być na kilka minut aż do wielogodzinnej nasiadówy. Nie jest to zwykle łatwa robota początkowo, ale z czasem idzie coraz łatwiej, a i przyjemności coraz więcej. Aktywność dotyczy w równym stopniu pytań i odpowiedzi. Można również uzupełniać opisy etykiet.

Wszystkie aktywności na StackOverflow są nagradzane punktami reputacji, odznakami i wiedzą w temacie. W zasadzie brak wad.

O StackOverflow (i GitHub, twitterze oraz w promocji reddit) będę mówił podczas mojej prezentacji StackOverflow, GitHub i twitter jako narzędzia profesjonalnego rozwoju programisty na DevCrowd’14. Gorąco zachęcam do udziału.

Wróćmy jednak do nauki nowej wersji Java 8.

Kiedy dzisiaj do mojej skrzynki trafiło zestawienie pytań z etykiety java-8 było w nim tylko jedno pytanie - How to dynamically do filtering in Java 8?. Niefortunnie, nie należy ono do najbardziej pouczających, ale odpowiedź już tak. Stuart Marks, który jest autorem odpowiedzi, postarał się o sporą dawkę wiedzy nt. lambd i "pochodnych" w Java 8. Zdecydowanie warto zapoznać się z odpowiedzią.

Weźmy chociażby "This can't be done with a simple filter(predicate) construct on a stream.”

java.util.Collection<E> jest podstawowym interfejsem w kolekcjach w Javie (od momentu pojawienia się ich, już w Java 1.2!). Dotyczy to struktur danych takich jak zbiory, listy, kolejki i mapy.

Zmianą w Java 8 jest dodanie m.in. metody default Stream<E> stream(), która wprowadza nas w świat strumieni. Kolekcja, na której wywołamy stream(), będzie źródłem danych.
Stream<Integer> ints = Arrays.asList(1,2,3).stream();
Mając Stream jesteśmy w domu. Drzwi “streamowe” otwarte.

Wezmę na tapetę pierwszą metodę z tercetu funkcyjnego - filter (obok map i reduce). Jej sygnatura to filter(Predicate predicate)
ints.filter(...)
Niestety moja wiedza dotycząca wartości przekazywanej do filter jest znikoma, bo pojawia się kolejna klasa z Java 8 - java.util.function.Predicate. Warto zajrzeć do javadoc, w którym napisano:

"This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.”

Jakaś masakra! :) Kompletnie mi to nic nie mówi. Intelekt i wdzięk podpowiadają mi jednak, że gdzieś tam pojawiały się konstrukcje w stylu argument(y) -> ciało funkcji. To właśnie nazywają lambdą (a przynajmniej takie mam wyobrażenie jak taka lambda mogłaby wyglądać mając pewne doświadczenie w innych językach funkcyjnych - Clojure, Scala, F#).

Sprawdzam taką konstrukcję.
Stream<Integer> oddInts = ints.filter(n -> n % 2 != 0);
Powinienem otrzymać strumień liczb nieparzystych. Wciąż to jednak wyłącznie strumień i próba System.out.println na tym zwróci jedynie tekstową reprezentację referencji.
java.util.stream.ReferencePipeline$2@682a0b20
Pora zmaterializować strumień do strawniejszej postaci, np. wyświetlę wszystkie elementy strumienia.

Z pomocą przychodzi mi IntelliJ IDEA. Wystarczy napisać oddInts.forEach i wcisnąć Ctrl+Shift+Spacja, aby pojawiła się jedyna słuszna podpowiedź - o -> {}. I to jest dokładnie to, czego potrzebuję - mam wybór, ale zbiór niezbyt liczny, bo jednego elementu. Próbuję wypisać elementy na ekran z System.out.println.
oddInts.forEach(o -> System.out.println(o));
Ctrl+Shift+F10 w IntelliJ IDEA i dostaję na ekranie wynik:
1
3
Dokładnie taki, jaki oczekiwałem!

Ale to nie koniec. IntelliJ IDEA nie poprzestaje i podpowiada, że warto zamienić tę konstrukcję na...method reference. Nie mam pojęcia, o co chodzi, ale podążam za głosem IDEA. Alt+Enter i już jest.
oddInts.forEach(System.out::println);
Taki sposób nauki lubię. Chciałbym mieć jeszcze możliwość poznawania Java 8 API w środowisku REPL (na wzór Scali, F#, Clojure czy Groovy), ale to raczej pieśń przyszłości. Dobrze, że mam IntelliJ IDEA!

Pełna klasa gotowa do uruchomienia:
package pl.japila.java8;

import java.util.Arrays;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream<Integer> ints = Arrays.asList(1, 2, 3).stream();
        Stream<Integer> oddInts = ints.filter(n -> n % 2 != 0);
        oddInts.forEach(System.out::println);
    }
}
A jak Tobie idzie poznawanie Java 8 API? Chętnie posłucham rad bardziej wytrwałych, którzy pierwsze dni mają już dawno za sobą.