Odkrywam na poważnie Javę 8. Jestem tak zachwycona strumieniami, że aż się muszę podzielić!
Przykład (Java 8)
Zacznę od przykładu. Napisałam ostatnio taki kod:
1 2 3 4 |
return map.entrySet().stream() .filter(entry -> entry.getValue() > 0) .map(entry -> entry.getKey()) .toArray(String[]::new); |
Co robi ten fragment? Zwraca tablicę zawierającą te klucze z mapy, których wartości są większe od 0.
Dokładniej:
- Odwołuje się do obiektu mapy (
map
). - Pobiera zbiór par klucz-wartość przechowywany w tej mapie (
entrySet()
). - Zamienia ten zbiór na strumień, albo, poprawniej, odczytuje ten zbiór poprzez strumień (
stream()
). - Filtruje strumień, pozostawiając w nim (a właściwie w nowym strumieniu) tylko te pary klucz-wartość, w których wartość jest większa od zera. Do filtrowania używana jest lambda, czyli definiowana w miejscu anonimowa funkcja zwracająca wartość typu
boolean
, przekazana jako parametr (.filter(entry -> entry.getValue() > 0)
). - Na wszystkich elementach (typu
Map.Entry
) pozostałych w strumieniu wykonywana jest funkcja zwracająca klucz (.map(entry -> entry.getKey())
). - Na samym końcu wywoływana jest metoda toArray z interfejsu
Stream
, do której przekazujemy generator (toArray(String[]::new)
), czyli funkcję, dzięki której strumień wie, ile pamięci zaalokować. Istnieje bezargumentowa wersja tej metody, tyle że przy jej użyciu otrzymamy tablicę obiektów typuObject
.
Przykład (Java 7 i starsze)
Po napisaniu tych paru linijek (ciągle mam problemy z formatowaniem takiego kodu i liczeniem linii) zaczęłam się zastanawiać, jak wyglądałby ten kod w wersji przedstrumieniowej. Musiałoby to być coś takiego:
1 2 3 4 5 |
List<String> list = new ArrayList<>(); for(Map.Entry<String, Integer> entry : map.entrySet()) if(entry.getValue() > 0) list.add(entry.getKey()); return list.toArray(new String[list.size()]); |
Porównanie
Kilka różnic:
Java 7 | Java 8 | |
Definicja zadania | Algorytm / opis procedury | Strumień przetwarzania / deklaracja oczekiwanego wyniku |
Kolejność przetwarzania | Sekwencyjna | Nieokreślona – może zostać poddana optymalizacji |
Typy danych | W niektórych miejscach, jak pętla for, trzeba je powtórzyć | Kompilator jest w stanie je wywnioskować, nie trzeba ich powtarzać (np. w definicjach lambd) |
Układ kodu | Kod z blokami i wcięciami | Łańcuch wywołań (oczywiście w przypadku bardziej złożonych lambd również trzeba stosować bloki) |
Uczciwie dodam, że po stronie minusów stosowania strumieni muszę na razie dopisać debugowanie. W przypadku wystąpienia problemu, odrobinę trudniej jest mi się zorientować, co dokładnie poszło nie tak.
Czy naprawdę nie było innego wolnego słowa?
Strumieni z Javy 8 (z pakietu java.util.stream
) nie należy mylić ze strumieniami służącymi do obsługi wejścia i wyjścia (z pakietu java.io
). Co za geniusz wymyślił tę nazwę – nie wiem. Być może chodziło i uniknięcie jeszcze gorszego słowa “monada”.
Jeśli chcesz wyszukać w Google informacji na temat “nowych” strumieni, używaj nazwy “Java 8 Streams”.
Co się dzieje w tle?
Strumień nie przechowuje własnych danych – można uznać go za “widok” na dane pochodzące z kolekcji lub innego źródła. Podczas potokowego przetwarzania danych na przykład za pomocą kilku kolejnych operacji map
, strumień nie jest modyfikowany, tylko zwracany jest wtedy nowy strumień, oferujący inne dane.
Wniosek
Wygodne. Używać.
Kiedy po raz pierwszy, w ramach prezentacji na Poznań JUG, zobaczyłam strumienie, nie byłam zachwycona. Pomyślałam sobie, że to kolejny wynalazek, który świetnie działa na wymyślonych, zabawkowych przykładach, a który okazuje się bezużyteczny w prawdziwym świecie. Nic bardziej mylnego, w pracy korzystam z nich teraz regularnie.
PS. Sklejanie Stringów
Od lat jeden z najbardziej irytujących braków w bibliotece standardowej Javy to sklejacz łańcuchów znaków. Teraz, jeśli chcesz za pomocą np. średników połączyć słowa przechowywane w kolekcji, nie już musisz pisać specjalnej funkcji ani załączać zewnętrznych bibliotek. Można tak:
1 |
String joined = list.stream().collect(Collectors.joining(";")); |