Jak nauczyć się Java Stream API [+5 Resources]

W Javie strumień to uporządkowany zbiór elementów, który umożliwia wykonywanie operacji sekwencyjnych lub równoległych.

W ramach strumienia można przeprowadzić dowolną liczbę transformacji pośrednich, a na końcu jedną operację finalną, która zwraca ostateczny rezultat.

Czym jest strumień?

Strumienie są obsługiwane za pomocą Stream API, wprowadzonego w Javie 8.

Wyobraźmy sobie strumień jako linię produkcyjną, gdzie surowce przechodzą przez kolejne etapy obróbki – są przygotowywane, sortowane, a na koniec pakowane. W kontekście Javy, tymi surowcami są obiekty lub kolekcje obiektów, operacjami są procesy takie jak przygotowanie, sortowanie i pakowanie, a cała linia produkcyjna stanowi właśnie strumień.

Strumień składa się z następujących komponentów:

  • Źródło danych
  • Operacje pośrednie
  • Operacja końcowa
  • Rezultat końcowy

Przyjrzyjmy się bliżej cechom strumieni w Javie:

  • Strumień nie jest strukturą danych przechowywaną w pamięci; stanowi raczej sekwencję elementów, takich jak tablice, obiekty lub zbiory obiektów, na których wykonywane są określone działania.
  • Strumienie działają w sposób deklaratywny, co oznacza, że określasz, co ma zostać zrobione, a nie jak to zrealizować.
  • Strumienie mogą być wykorzystane tylko jednorazowo, gdyż nie zachowują danych.
  • Strumień nie zmienia pierwotnej struktury danych; zamiast tego generuje nową strukturę na jej podstawie.
  • Rezultat końcowy jest uzyskiwany poprzez wykonanie operacji terminalnej na końcu potoku.

Stream API a Przetwarzanie Kolekcji

Kolekcja to struktura danych w pamięci, która służy do przechowywania i manipulowania danymi. Kolekcje oferują różne typy struktur, takie jak zbiory, mapy, listy, umożliwiające przechowywanie danych. Natomiast strumień to narzędzie do efektywnego przesyłania danych przez potok w celu ich przetworzenia.

Oto przykład użycia kolekcji ArrayList:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(0, 3);
        System.out.println(list);
    }
}

Output: 
[3]

W powyższym przykładzie widać, jak można utworzyć kolekcję ArrayList, dodać do niej dane i następnie wykonywać na nich operacje za pomocą dostępnych metod.

Za pomocą strumienia można przetwarzać istniejącą strukturę danych i generować nową, zmodyfikowaną wartość. Poniżej przykład, jak utworzyć kolekcję ArrayList i przefiltrować ją za pomocą strumienia.

import java.util.ArrayList;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList();

        for (int i = 0; i < 20; i++) {
            list.add(i+1);
        }

        System.out.println(list);

        Stream<Integer> filtered = list.stream().filter(num -> num > 10);
        filtered.forEach(num -> System.out.println(num + " "));
    }
}

#Output

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 

W powyższym przykładzie strumień tworzony jest na bazie istniejącej listy, która jest następnie iterowana w celu odfiltrowania wartości większych niż 10. Należy zauważyć, że strumień nie przechowuje żadnych danych, a jedynie iteruje po liście i wypisuje wynik. Próba bezpośredniego wydrukowania strumienia skutkuje wyświetleniem odwołania do strumienia, a nie jego zawartości.

Wykorzystanie Java Stream API

Java Stream API przyjmuje kolekcję elementów lub ich sekwencję jako źródło, a następnie wykonuje na nich serie operacji w celu uzyskania finalnego rezultatu. Strumień można traktować jako potok, przez który przepływają elementy i są one poddawane różnym transformacjom.

Strumień można stworzyć z różnych źródeł, takich jak:

  • Kolekcje, na przykład listy lub zbiory.
  • Tablice.
  • Pliki i ścieżki dostępu do nich, przy użyciu bufora.

W strumieniu można wyróżnić dwa typy operacji:

  • Operacje pośrednie
  • Operacje końcowe (terminalne)

Operacje Pośrednie a Operacje Terminalne

Każda operacja pośrednia generuje nowy strumień, który przetwarza dane wejściowe za pomocą określonej metody. Dane nie są od razu przetwarzane, tylko przepływają przez kolejne strumienie. Dopiero operacja terminalna uruchamia przetwarzanie strumienia w celu uzyskania rezultatu.

Przykładowo, posiadając listę 10 liczb, którą chcemy przefiltrować, a następnie zmapować, poszczególne elementy nie zostaną od razu sprawdzone i przetworzone. Zamiast tego, elementy będą analizowane jeden po drugim i, jeśli spełnią warunek, zostaną zmapowane, tworząc nowe strumienie dla każdego z nich.

Operacja mapowania jest wykonywana na pojedynczych elementach, które przeszły przez filtr, a nie na całej liście. Wyniki tych operacji są łączone w jeden rezultat dopiero podczas operacji terminalnej.

Po wykonaniu operacji terminalnej strumień zostaje „zużyty” i nie można go już dalej wykorzystać. Aby ponownie wykonać te same operacje, konieczne jest utworzenie nowego strumienia.

Źródło: The Bored Dev

Mając podstawowe zrozumienie działania strumieni, przejdźmy do szczegółów ich implementacji w Javie.

#1. Pusty Strumień

Pusty strumień można utworzyć, korzystając z metody `empty()` interfejsu Stream API.

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream emptyStream = Stream.empty();
        System.out.println(emptyStream.count());
    }
}

Output:
0

Powyższy kod pokazuje, że jeśli sprawdzimy liczbę elementów w pustym strumieniu, otrzymamy wartość 0. Puste strumienie są przydatne w unikaniu wyjątków `NullPointerException`.

#2. Tworzenie Strumienia z Kolekcji

Kolekcje takie jak `List` i `Set` oferują metodę `stream()`, która pozwala na utworzenie strumienia na bazie danej kolekcji. Utworzony strumień można następnie przetwarzać w celu uzyskania finalnego efektu.

ArrayList<Integer> list = new ArrayList();

for (int i = 0; i < 20; i++) {
    list.add(i+1);
}

System.out.println(list);

Stream<Integer> filtered = list.stream().filter(num -> num > 10);
filtered.forEach(num -> System.out.println(num + " "));

#Output

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 

#3. Tworzenie Strumienia z Tablicy

Metoda `Arrays.stream()` służy do tworzenia strumienia z tablicy.

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] stringArray = new String[]{"this", "is", "newsblog.pl"};
        Arrays.stream(stringArray).forEach(item -> System.out.print(item + " "));
    }
}

#Output

this is newsblog.pl 

Można również określić początkowy i końcowy indeks elementów tablicy, z których chcemy utworzyć strumień. Indeks początkowy jest włączony, natomiast indeks końcowy jest wyłączony.

String[] stringArray = new String[]{"this", "is", "newsblog.pl"};
Arrays.stream(stringArray, 1, 3).forEach(item -> System.out.print(item + " "));

Output:
is newsblog.pl

#4. Znajdowanie Minimum i Maksimum za Pomocą Strumieni

Za pomocą komparatorów w Javie można uzyskać dostęp do maksymalnej i minimalnej wartości w kolekcji lub tablicy. Metody `min()` i `max()` przyjmują komparator jako argument i zwracają obiekt `Optional`.

Obiekt `Optional` to kontener, który może zawierać wartość różną od `null` lub być pusty. Jeśli `Optional` zawiera wartość różną od `null`, wywołanie na nim metody `get()` zwróci tę wartość.

import java.util.Arrays;
import java.util.Optional;

public class MinMax {
    public static void main(String[] args) {
        Integer[] numbers = new Integer[]{21, 82, 41, 9, 62, 3, 11};

        Optional<Integer> maxValue = Arrays.stream(numbers).max(Integer::compare);
        System.out.println(maxValue.get());

        Optional<Integer> minValue = Arrays.stream(numbers).min(Integer::compare);
        System.out.println(minValue.get());
    }
}

#Output
82
3

Materiały Edukacyjne

Po zdobyciu podstawowej wiedzy o strumieniach w Javie, przedstawiamy 5 materiałów, które pomogą Ci lepiej zrozumieć temat Javy 8:

#1. Java 8 w Akcji

Ta książka to przewodnik po nowych funkcjach Javy 8, w tym strumieniach, wyrażeniach lambda i programowaniu funkcyjnym. Zawiera również quizy i zadania sprawdzające wiedzę, które pomogą utrwalić zdobyte informacje.

Książkę można nabyć w formie papierowej lub jako audiobook na Amazonie.

#2. Java 8 Lambdas: Programowanie Funkcyjne dla Wszystkich

Ta książka jest skierowana do programistów Java SE i wyjaśnia, w jaki sposób wprowadzenie wyrażeń Lambda wpływa na język Java. Zawiera przystępne wyjaśnienia, ćwiczenia i przykłady kodu, które ułatwiają opanowanie wyrażeń lambda w Javie 8.

Dostępna jest w formie papierowej oraz w wersji na Kindle na Amazonie.

#3. Java SE 8 dla Naprawdę Niecierpliwych

Ta książka jest przeznaczona dla doświadczonych programistów Java SE i omawia usprawnienia wprowadzone w Javie SE 8, w tym Stream API, wyrażenia lambda, zmiany w programowaniu współbieżnym oraz kilka mało znanych funkcji z Javy 7.

Książka dostępna jest tylko w wersji papierowej na Amazonie.

#4. Nauka Programowania Funkcyjnego w Javie z Wyrażeniami Lambda i Strumieniami

Ten kurs na Udemy omawia podstawy programowania funkcyjnego w Javie 8 i 9. Wyrażenia lambda, odwołania do metod, strumienie i interfejsy funkcyjne są głównymi tematami kursu.

Kurs zawiera również zadania i ćwiczenia związane z programowaniem funkcyjnym.

#5. Biblioteka Klas Javy

Biblioteka Klas Javy jest częścią specjalizacji Core Java oferowanej przez Coursera. Kurs uczy, jak pisać kod bezpieczny typowo przy użyciu Java Generics, jak korzystać z biblioteki składającej się z ponad 4000 klas, jak pracować z plikami oraz jak zarządzać błędami w czasie wykonywania. Aby wziąć udział w kursie, wymagane są pewne umiejętności:

  • Wprowadzenie do Javy
  • Wprowadzenie do programowania obiektowego w Javie
  • Hierarchie obiektowe w Javie

Podsumowanie

Java Stream API oraz wprowadzenie funkcji Lambda w Javie 8 uprościły i usprawniły wiele aspektów programowania w Javie, takich jak iteracje równoległe, interfejsy funkcyjne, zmniejszenie ilości kodu itp.

Strumienie mają jednak swoje ograniczenia; największym z nich jest fakt, że można ich użyć tylko raz. Jeśli programujesz w Javie, powyższe materiały pomogą ci lepiej zrozumieć te zagadnienia, dlatego warto się z nimi zapoznać.

Może cię również zainteresować temat obsługi wyjątków w Javie.