Miesięczne archiwum: Kwiecień 2015

Hashtag

Ponieważ sporo ludzi nie ogarnia, tu jest krótkie obrazkowe wyjaśnienie:

hashtag

Hash to jest znak #.

Tag (etykieta) to jest system kategoryzacji oparty o nadawanie rzeczom etykiet, gdzie tych etykiet może być zero, jedna lub kilka dla danej rzeczy.

Hashtag to etykieta poprzedzona znakiem #.

Wrażenia z używania Stream API w Javie 8

Ostatnio zapoznałem się z API strumieni wprowadzonego w Javie 8. Wprowadzenie API w stylu funkcjonalnym do języka Java to wspaniała wiadomość – język idzie w dobrym kierunku.

Użycie jest nawet proste – najpierw tworzymy strumień:

Dla kolekcji:

kolekcja.stream()

Dla tablic:

Arrays.stream(tablica)

Dla jednej lub kilku wartości

Stream.of("aaa", "bbb", "ccc") // strumienie obiektów
IntStream.of(4, 5, 7, 42) // strumienie intów
DoubleStream.of(2.5, 4.5) // strumienie doubli

Mamy tu pewien rodzaj duplikacji, wymuszony „specjalnym” traktowaniem typów wbudowanych. Niestety, taka jest rzeczywistość tego języka progamowania i jedyny sposób żeby się pozbyć tej duplikacji wymagałby wprowadzenia ulepszonych generyków. Zauważ że jest 8 value types (boolean, int, long, char, byte, float, double, short), ale strumieni operujących na wartościach jest 4. Podejrzewam że pisanie tych wszystkich klas było dla nich wystarczająco irytujące, a zyski na tyle niewielkie (możesz użyć IntStream dla char i short, DoubleStream dla float), że po prostu sobie darowali.

Po utworzeniu takiego strumienia można na nim wywoływać operacje, które mogą, ale nie muszą zamknąć strumień. Po zamknięciu strumienia strumienia nie można go ponownie otworzyć lub przewinąć na początek – tak jak iteratory które nie są ForwardIteratorami w C++ lub iteratory w Pythonie.

Dzięki temu że operacje nie-terminujące zwracają strumień, można łączyć wywołania w łańcuch, o tak:

List<Integer> l = kolekcja.stream()
                          .filter((x) -> x < 5)
                          .map((x) -> x + 5)
                          .collect(Collectors.toList());

Tutaj mamy przykładową operację na strumieniach, których schemat można przedstawić tak:

  1. Tworzymy strumień
  2. Wołamy kolejne operacje
  3. Zbieramy wyniki

Spora część operacji jest leniwa, dzięki czemu można operować na strumieniach nieskończonych – zupełnie na listach w Haskellu. Nie wszystkie operacje są leniwe – w razie wątpliwości polecam skonsultować się z dokumentacją.

Podstawowe operatory funkcjonalne są – mamy map, reduce i filter. W przypadku map trzeba zwracać uwagę na typ wartości zwracanej, na co się nadziałem na sam start – jeżeli mamy strumień obiektów i to map() musimy użyć z funkcją Object -> Object. Jak mamy funkcje co przyjmuje prymityw i zwrca Object albo na odwrót, to musimy użyć odpowiednich operacji dla tych funkcji.

stream.map(/* funkcja Object -> Object */) // zwraca Stream
stream.mapToInt(/* funkcja Object -> int */) // zwraca IntStream
stream.mapToDouble(/* funkcja Object -> double */) // zwraca DoubleStream
stream.mapToLong(/* funkcja Object -> long */) // zwraca LongStream
int_stream.map(/* funkcja int -> int */) // zwraca IntStream
int_stream.mapToObj(/* funkcja int -> Object */) // zwraca Stream

Nie ma tu za krzty generyczności, o czym świadczy fakt, że te wszystkie klasy ktoś musiał napisać te wszystkie klasy, metody i interfejsy (każda funkcja map* przyjmuje osobny interfejs funkcjonalny). Co gorsza, nie ma metod Stream.mapToObj ani IntStream.mapToInt, więc musisz się też martwić jaki jest strumień po lewej stronie i użyć odpowiedniej nazwy.

Na koniec zbieramy wyniki: na przykład poprzez sum, collect, reduce, count. first. Z tych dwóch na uwagę zasługują collect oraz reduce:

  • reduce jest w dwóch postaciach: dwuargumentowej (T, T -> T -> T) zwracającej T będący redukcją, lub jeżeli jest pusty, lewy argument; oraz jednoargumentowej zwracającej Optional<T>, ponieważ null jest bezużyteczny – nie można na nim wołać metod ani robić cokolwiek sensownego poza porównaniem z innym obiektem.
  • collect to ogólne pojęcie na „zbierz te wszystkie wartości i zwróć mi je w tej postaci”. Mamy metodę collect która przyjmuje trzy operacje: utwórz wartość początkową, dodaj coś, złącz wartości, które dla np. Collections API odpowiadają funkcjom List::new, List::add i List::addAll. Ponieważ te trzy operacje są ściśle połączone, interfejs Collector łączy je w jedną całość jako jeden kolektor który można przekazywać. Gotowe kolektory można znaleźć w java.util.Stream.Collectors:

    • Collectors.toList() – zwraca listę ktora zawiera elementy ze strumienia (nie ma żadnych gwarancji której klasy implementującej List<T> obiekt zostanie zwrócony)
    • Collectors.toCollection() – zwraca kolekcje konkretnej klasy utworzonej przez funkcje podaną jako argument (np. ArrayList<T>::new)

Podsumowując, Java 8 Stream API jest bardzo uniwersalne i będę je używał wszędzie gdzie się da 😛