Kurs Java

Klasa Optional

Appa Notka. Uwaga. Od 16/01/2022 ten rozdział kursu zostaje udostępniony za darmo! Znajdziesz tutaj teorię oraz ponad 10 opisanych przykładów użycia klasy Optional. Miłej lektury!
W Javie 8 pojawia się klasa, która umożliwia opakowanie wartości niezależnie od tego, czy ta wartość faktycznie istnieje, czy też jest nullem. To bardzo praktyczne rozwiązanie pozwala na uniknięcie wszechobecnego przed Javą 8 sprawdzania, czy wartość jest różna od null, by następnie wykonać na niej jakąś operację.

Za każdym razem, gdy spodziewaliśmy się braku wartości, byliśmy zobowiązani do stworzenia warunku, jak na poniższym przykładzie:
if (item != null) {
    ...item.getName();
}
Na szczęście ktoś w końcu wpadł na pomysł, że brak wartości jest informacją cenną samą w sobie i należy udostępnić rozwiązanie wspierające obsługę takiej sytuacji. Tak oto w Javie 8 pojawiła się klasa Optional, która opakowuje wartość, z jaką pracujemy w danym miejscu w kodzie. Dopuszcza się, że wartość ta będzie dostępna w obiekcie klasy Optional lub nie.

Po zajrzeniu do wnętrza klasy można zauważyć, że zawiera ona pole do przechowywania wartości, jak również kilka metod operujących na tym polu. Jedną z metod jest metoda isPresent, która sprawdza, czy wartość istnieje. Wykonuje ona za nas dokładnie to samo sprawdzenie, które przez lata znajdowaliśmy w kodzie każdego programu Java, a więc weryfikację czy wartość jest różna od null:
public final class Optional<T> {

    private static final Optional<?> EMPTY = new Optional<T>();

    private final T value;

    private Optional() {
        this.value = null;
    }

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    public T get() {
        if (value == null) {
            throw new NoSuchElementException("No value present");
        }
        return value;
    }

    public boolean isPresent() {
        return value != null;
    }

    ...
}
Kolejna metoda - get - odpowiada za pobranie wartości. Wykonujemy ją po wcześniejszym sprawdzeniu, czy wartość jest dostępna, gdyż jeśli wartość nie będzie istniała, a my uruchomimy metodę get, wówczas otrzymamy wyjątek NoSuchElementException.
...
if(someTextOptional.isPresent()) {
    String text = someTextOptional.get();
}

Tworzenie obiektu klasy Optional

Analizując kod klasy, napotkamy tam także metody do tworzenia obiektu typu Optional. Istnieją dwie takie metody, obie są statyczne. Jedna - of - zakłada, że nasza wartość nie będzie nullem, a jeśli przez pomyłkę prześlemy tam null, wówczas zostanie on odrzucony przez metodę walidacyjną requireNonNull z klasy Objects. W przeciwnym wypadku zostanie utworzony nowy obiekt klasy Optional z ustawioną wartością.
public final class Optional<T> {

    ...

    public static <T> Optional<T> of(T value) {
        return new Optional<>(value);
    }

    ...
}
Druga metoda - ofNullable - pozwala na przekazanie do niej null, ale wtedy zostanie zwrócony obiekt klasy Optional, który jest pusty i jest zdefiniowany za pomocą stałej o nazwie EMPTY. Oczywiście, jeśli do metody ofNullable zamiast null zostanie przekazany istniejąca wartość, wtedy zostanie utworzony nowy obiekt klasy Optional z tą ustawioną wartością:
public final class Optional<T> {

    ...

    private static final Optional<?> EMPTY = new Optional<T>();

    public static<T> Optional<T> empty() {
        @SuppressWarnings("unchecked")
        Optional<T> t = (Optional<T>) EMPTY;
        return t;
    }

    public static <T> Optional<T> ofNullable(T value) {
        return value == null ? empty() : of(value);
    }

    ...
}
Klasa Optional jest tak zaprojektowana, by zablokować programiście możliwość utworzenia instancji tej klasy bez użycia wspomnianych metod statycznych of i ofNullable. Zostało to osiągnięte przez ukrycie widoczności konstruktorów poza ciałem klasy. Dodatkowo sama klasa oznaczona jest modyfikatorem final:
public final class Optional<T> {

    ...

    private Optional() {
        this.value = null;
    }

    ...

    private Optional(T value) {
        this.value = Objects.requireNonNull(value);
    }

    ...
}

Optional - Tworzenie metodą of

Po tym sporym wstępie przyszedł czas na pierwszy przykład. Zademonstrujemy tworzenie obiektu typu Optional oraz sprawdzenie, czy obiekt ten zachowuje się zgodnie z oczekiwaniami.
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        Optional<String> itemNameOptional = Optional.of("Appa Item");
        if (itemNameOptional.isPresent()) {
            System.out.println("Optional value:");
            System.out.println(itemNameOptional.get());
        }

        // Metoda rzuci wyjątkiem
        Optional<String> anythingOptional = Optional.of(null);
    }
}
Najpierw tworzymy obiekt klasy Optional w zalecany sposób, a więc używając metody of z podaną wartością. W tym przypadku jest to obiekt typu String. Sprawdzamy, czy wartość istnieje, a następnie pobieramy ją z obiektu opakowującego. W kolejnej próbie wykorzystujemy metodę of w niepoprawny sposób, ponieważ przekazujemy do niej null. Powoduje to wyrzucenie wyjątku NullPointerException:
Java 8 - Wyjątek z Optional.of

Optional - Tworzenie metodą ofNullable

Drugi sposób tworzenia obiektu typu Optional polega na użyciu metody ofNullable.
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        Optional<String> itemNameOptional = Optional.ofNullable("Appa Item");
        if (itemNameOptional.isPresent()) {
            System.out.println("Optional value:");
            System.out.println(itemNameOptional.get());
        }

        // Metoda isPresent zwróci false
        Optional<String> nullOptional = Optional.ofNullable(null);
        if (nullOptional.isPresent()) {
            System.out.println("Optional value:");
            System.out.println(nullOptional.get());
        }
        else {
            System.out.println("There is no value in Optional");
        }
    }
}
Wynik wykonania kodu:
Java 8 - Optional 1
Pierwsza część zadziała podobnie jak wcześniej. Na wydruku pojawi się pobrana wartość. Natomiast stworzenie obiektu z nullem zachowa się inaczej niż w przypadku metody of. Tym razem dozwolone jest przekazanie nulla do metody, a sprawdzenie warunku za pomocą metody isPresent zwróci false. W związku z tym nie dojdzie do pobrania wartości, a jedynie zwrócony zostanie tekst zdefiniowany przez nas w ramach klauzuli else. Wykonanie całego kodu będzie miało wynik jak na obrazku.
Appa Notka. Klasa Optional zawiera jeszcze kilka innych metod, które są bardzo istotne, na przykład orElse, orElseGet, orElseThrow. Będziemy je omawiać w dalszej części tego rozdziału, już na kodzie konkretnych przykładów bazujących na strumieniach.

Optional - w strumieniu

No dobrze, wiesz już, jak wygląda Optional i poznałe(a)ś dwa proste przykłady jego użycia. Czas zaprezentować jak ta doskonale klasa ta współpracuje ze strumieniami w Javie. Na początek przykład na prostych obiektach. Uruchamiamy metodę klasy Stream, która znajduje pierwszy element w strumieniu. Jeśli taki element istnieje, wtedy metoda zwróci pierwszą wartość, opakowaną w obiekt klasy Optional. Pozostaje nam sprawdzić, czy wartość jest faktycznie dostępna, a jeśli tak, pobrać ją z optionala za pomocą metody get.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<String> itemsNames = Arrays.asList("AppaItem no. 1", "AppaItem no. 2", "AppaItem no. 3");
        Optional<String> firstItemNameOptional = itemsNames.stream().findFirst();
        if (firstItemNameOptional.isPresent()) {
            System.out.println("First item:");
            System.out.println(firstItemNameOptional.get());
        }
    }
}
Wynik wykonania kodu:
Java 8 - Optional w strumieniu

Optional - jeśli wartość nie istnieje

W poprzednim przykładzie wartość w optionalu istniała, ponieważ metoda findAny znalazła ją w strumieniu. W przypadku gdy w strumieniu nie ma żadnej wartości, wówczas możemy na przykład rzucić wyjątek w bloku else klazuzuli if - else:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<String> itemsNames = new ArrayList<>();
        Optional<String>  noItemNameOptional = itemsNames.stream().findFirst();
        useOptional(noItemNameOptional);
    }

    // Zakładam, że dostaję obiekt klasy Optional i nie wiem czy wartość jest w środku czy nie.
    public static void useOptional(Optional<String>  itemNameOptional) {

        if (itemNameOptional.isPresent()) {
            System.out.println("First item:");
            System.out.println(itemNameOptional.get());
        } else {
            throw new RuntimeException("Item name is missing!");
        }
    }
}
Wynik wykonania kodu:
Java 8 - Optional 2

Optional - orElseThrow

Zamiast samemu obsługiwać wyrzucenie wyjątku, możemy zatrudnić do tego zadania kolejną metodę klasy Optional o nazwie orElseThrow. Metoda ta przyjmuje parametr w postaci wyrażenia lambda, a dokładniej implementacji interfejsu funkcyjnego Supplier. Zwracamy tam wyjątek, który powinien zostać wyrzucony, jeśli wartość w optionalu nie zostanie znaleziona.
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<String> itemsNames = new ArrayList<>();
        Optional<String> noItemNameOptional = itemsNames.stream().findFirst();
        useOptional(noItemNameOptional);
    }

    public static void useOptional(Optional<String> itemNameOptional) {

        if (itemNameOptional.isPresent()) {
            System.out.println("First item:");
        }

        System.out.println(itemNameOptional
                                .orElseThrow(() -> new RuntimeException("Item name is missing!")));
    }
}
Wynik wykonania kodu:
Java 8 - Optional 3

Optional - orElse

W przypadku gdy optional nie zawiera wartości, możemy też zwrócić wartość awaryjną (domyślną). Używamy wtedy kolejnej metody z klasy Optional, metody orElse:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<String> itemsNames = new ArrayList<>();
        Optional<String> noItemNameOptional = itemsNames.stream().findFirst();
        useOptional(noItemNameOptional);
    }

    public static void useOptional(Optional<String> itemNameOptional) {

        if (itemNameOptional.isPresent()) {
            System.out.println("First item:");
        }

        System.out.println(itemNameOptional.orElse("Item name is missing!"));
    }
}
Wynik wykonania kodu:
Java 8 - Optional 4

Optional - orElseGet

Istnieje jeszcze inna możliwość obsługi alternatywnej ścieżki. Przydaje się, gdy chcemy wykonać algorytm dostarczający pewne rozwiązanie, jeśli wartość w optionalu nie istnieje. Wtedy możemy zdefiniować obiekt interfejsu Supplier i przekazać go do metody orElseGet:
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

public class Start {

    public static void main(String[] args) {

        List<String> itemsNames = new ArrayList<>();
        Optional<String> noItemNameOptional = itemsNames.stream().findFirst();
        useOptional(noItemNameOptional);
    }

    public static void useOptional(Optional<String> itemNameOptional) {

        if (!itemNameOptional.isPresent()) {
            System.out.println("Item name is missing! We have another string for you:");
        }

        System.out.println(itemNameOptional.orElseGet(() -> UUID.randomUUID().toString()));
    }
}
Wynik wykonania kodu:
Java 8 - Optional 5

Optional - w metodzie filter

W rozdziale Strumienie - Filtry przygotowaliśmy kilka przykładów na działanie filtrów w strumieniach. Przy okazji omawiania optionali, warto jest dodać do tego jeszcze jeden przykład. Najpierw stworzymy klasę MovieItem, po czym zbudujemy kod zasadniczej części programu:
import java.util.Optional;

public class MovieItem {

    private String title;
    private String genre;

    public MovieItem(String title, String genre) {
        this.title = title;
        this.genre = genre;
    }

    public String getTitle() {
        return title;
    }

    public Optional<String> getGenre() {
        return Optional.ofNullable(genre);
    }

    @Override
    public String toString() {
        return title;
    }
}
Poniżej przetwarzany dane ze strumienia z filmami. Naszym celem jest wykonanie dwóch przeciwstawnych operacji. Najpierw przepuszczamy dalej tylko te obiekty, które mają ustawioną nazwę gatunku, a następnie wybieramy tylko te, którym tej nazwy brakuje (null został przekazany do nich przez konstruktor). W drugim przypadku zaprzeczamy warunek za pomocą znaku wykrzyknika:
import java.util.Arrays;
import java.util.List;

public class Start {

    public static void main(String[] args) {

        List<MovieItem> movies = Arrays.asList(new MovieItem("Parasite", "Dramat"),
                                               new MovieItem("Kac Vegas", "Komedia"),
                                               new MovieItem("Bękarty wojny", null),
                                               new MovieItem("Zielona mila", "Dramat"),
                                               new MovieItem("Chłopaki nie płaczą", "Komedia"),
                                               new MovieItem("American Pie", null));

        System.out.println("Movies with genre:");
        movies.stream()
                .filter(movieItem -> movieItem.getGenre().isPresent())
                .forEach(System.out::println);

        System.out.println("\nMovies without genre:");
        movies.stream()
                .filter(movieItem -> !movieItem.getGenre().isPresent())
                .forEach(System.out::println);
    }
}
Wynik wykonania kodu:
Java 8 - Optional 6

Optional - filtracja z referencją do metody isPresent

Klasa MovieItem do bieżącego przykładu wygląda dokładnie tak samo, jak poprzednio.
import java.util.Optional;

public class MovieItem {

    private String title;
    private String genre;

    public MovieItem(String title, String genre) {
        this.title = title;
        this.genre = genre;
    }

    public String getTitle() {
        return title;
    }

    public Optional<String> getGenre() {
        return Optional.ofNullable(genre);
    }

    @Override
    public String toString() {
        return title;
    }
}
Teraz jednak wykonamy trochę inny algorytm. Naszym celem będzie udokumentowanie użycia referencji do metody isPresent z klasy Optional. Zresztą w ogóle użyjemy samych referencji do metod.

Najpierw mapujemy obiekt klasy MovieItem na obiekt typu Optional (poprzez wyciągnięcie go z obiektu filmu). Następnie naszą tytułową metodą sprawdzamy, czy istnieje wartość w optionalu (filtrujemy), po czym wywołujemy metodę get (znowu mapujemy), by tę wartość wyciągnąć.

Na tym etapie mamy już wszystkie wartości, ale niektóre z nich będą się duplikować ("Dramat" i "Komedia" są określone dla dwóch filmów). Potrzebujemy pozbyć się duplikatów, więc uruchamiamy metodę distinct na strumieniu. Tak określony strumień ze zmapowanymi, odfiltrowanymi i niepowtarzającymi się wartościami poddajemy działaniu metody forEach, która drukuje pozostałe na tym etapie wartości.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<MovieItem> movies = Arrays.asList(new MovieItem("Parasite", "Dramat"),
                                               new MovieItem("Kac Vegas", "Komedia"),
                                               new MovieItem("Bękarty wojny", null),
                                               new MovieItem("Zielona mila", "Dramat"),
                                               new MovieItem("Chłopaki nie płaczą", "Komedia"),
                                               new MovieItem("American Pie", null));

        System.out.println("Unique genres:");
        movies.stream()
                .map(MovieItem::getGenre)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .distinct()
                .forEach(System.out::println);
    }
}
Wynik wykonania kodu:
Java 8 - Optional 7

Optional - zaprzeczenie referencji do metody isPresent

Z referencją do metody isPresent i ogólnie z referencjami do metod logicznych wiąże się jeden problem. Nie da się ich bezpośrednio zaprzeczyć poprzez użycie znaku wykrzyknika. Coś takiego się nam nie skompiluje:
.filter(!Optional::isPresent) // tak nie zaprzeczymy
Zatem, co można zrobić w takiej sytuacji? Wtedy możemy skonstruować metodę w testowanym obiekcie, która zwróci PRAWDĘ, jeśli wartość nie jest dostępna w optionalu. W ciele metody korzystamy z zaprzeczenia metody isPresent, co przełoży się na potwierdzenie braku wartości:
import java.util.Optional;

public class MovieItem {

    private String title;
    private String genre;

    public MovieItem(String title, String genre) {
        this.title = title;
        this.genre = genre;
    }

    public String getTitle() {
        return title;
    }

    public Optional<String> getGenre() {
        return Optional.ofNullable(genre);
    }

    public boolean isEmptyGenre() {
        return !Optional.ofNullable(genre).isPresent();
    }

    @Override
    public String toString() {
        return title;
    }
}
Teraz wystarczy już tylko użyć referencji do metody isEmptyGenre i nie musimy się więcej martwić o zaprzeczanie dostępności gatunku. Sumarycznie, patrząc na zmiany w obu klasach, musieliśmy napisać trochę dodatkowego kodu, ale za to dostajemy rozwiązanie wielokrotnego użytku.
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class Start {

    public static void main(String[] args) {

        List<MovieItem> movies = Arrays.asList(new MovieItem("Parasite", "Dramat"),
                                               new MovieItem("Kac Vegas", "Komedia"),
                                               new MovieItem("Bękarty wojny", null),
                                               new MovieItem("Zielona mila", "Dramat"),
                                               new MovieItem("Chłopaki nie płaczą", "Komedia"),
                                               new MovieItem("American Pie", null));

        System.out.println("\nMovies without genre:");
        movies.stream()
              .filter(MovieItem::isEmptyGenre)
              .forEach(System.out::println);
    }
}
Wynik wykonania kodu:
Java 8 - Optional w strumieniu

Optional - filtracja z ifPresent

W klasie Optional istnieje jeszcze metoda, która pozwala od razu na wykonanie określonego zadania, jeśli tylko wartość jest dostępna. Metoda ta nazywa się ifPresent i przyjmuje parametr w postaci implementacji interfejsu funkcyjnego Consumer. Przykładem takiej implementacji jest referencja do metody println w poniższym przykładzie:
import java.util.Arrays;
import java.util.List;

public class Start {

    public static void main(String[] args) {

        List<MovieItem> movies = Arrays.asList(new MovieItem("Parasite", "Dramat"),
                                               new MovieItem("Kac Vegas", "Komedia"),
                                               new MovieItem("Bękarty wojny", null),
                                               new MovieItem("Zielona mila", "Dramat"),
                                               new MovieItem("Chłopaki nie płaczą", "Komedia"),
                                               new MovieItem("American Pie", null));

        System.out.println("Not empty and not unique genres:");
        movies.stream()
                .map(MovieItem::getGenre)
                .forEach(genre -> genre.ifPresent(System.out::println));

        System.out.println("\nNot empty unique genres:");
        movies.stream()
                .map(MovieItem::getGenre)
                .distinct()
                .forEach(genre -> genre.ifPresent(System.out::println));
    }
}
Wynik wykonania kodu:
Java 8 - Optional w strumieniu
Pełne zrozumienie działania klasy Optional jest szalenie ważne. Wynika to z tego, że regularnie, w różnych miejscach kodu, jesteśmy zmuszeni do podejmowania decyzji warunkowych oraz filtracji danych. Pewnym wyzwaniem może być przyzwyczajenie się do rezygnacji ze sprawdzenia, czy dana wartość jest różna od null, które przez lata przewijało się w naszych programach. W szerszej perspektywie jest to jednak całkiem przyjemna zmiana.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
kursjava@javappa.com

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE