Optional orElse vs orElseGet

Na tę pułapkę często łapią się programiści Java, nawet Seniorzy! Poznaj różnicę między metodami orElse a orElseGet i korzystaj z Optionala świadomie

Optional orElse vs orElseGet

Po co nam Optional?

Przed Javą w wersji 8 mieliśmy tylko dwa sposoby na napisanie metody, która w pewnych okolicznościach nie mogła zwrócić żadnego obiektu:

  1. Zwrócić null
  2. Wyrzucić wyjątek

Żadne z tych rozwiązań nie było perfekcyjne. Wyjątki powinny być zarezerwowane dla wyjątkowych sytuacji. Poza tym ich wyrzucanie jest kosztowne, ponieważ przechwytywany jest cały ślad stosu (ang. Stack trace) podczas tworzenia wyjątku. Z kolei zwracanie z metody null powoduje, że klient (klasa wywołująca tę metodę) musi dodać sprawdzenie, czy zwrócony obiekt nie jest null. Jeżeli tego nie zrobi, a metoda zwróci null, zostanie wyrzucony wyjątek NullPointerException przy próbie operacji na tym pustym obiekcie.

W Javie 8 został dodany trzeci sposób: klasa Optional<T>, która reprezentuje niezmienny (ang. Immutable) kontener opakowujący referencję do obiektu T lub nie opakowujący nic. Gdy Optional nic nie opakowuje, jest po prostu pusty (ang. Empty).

Zaczynając od Javy 8 metoda, która w pewnych okolicznościach nie może zwrócić żadnego obiektu powinna zwracać Optional<T>. Wyjątkiem są tu kolekcje, mapy, streamy, tablice i inne typy kontenerowe. W takim wypadku lepiej po prostu zwrócić je puste niż opakowane Optionalem. Optional nie powinien także opakowywać innego Optionala.

Jak stworzyć Optionala?

Optional może zostać stworzony na trzy sposoby. Każdy z nich wymaga wywołania innej metody. Poniżej ich sygnatury:

public static <T> Optional<T> empty()

public static <T> Optional<T> of(T value)

public static <T> Optional<T> ofNullable(T value)

Wywołanie metody empty() stworzy pustego Optionala, czyli nie opakowującego żadnego obiektu.

Wywołanie metody of(T value) stworzy Optionala opakowującego obiekt T. Metoda ta powstała z myślą przekazywania obiektu nie będącego null. Jeżeli będzie, zostanie wyrzucony wyjątek NullPointerException.

Wywołanie metody ofNullable(T value) stworzy Optionala opakowującego obiekt T, jeżeli obiekt ten nie jest null. W przypadku, gdy obiekt T będzie null, zostanie stworzony pusty Optional. Jest to tak jakby kombinacja dwóch poprzenich metod.

Podsumowując:

  • empty() używamy do stworzenia pustego Optionala
  • of(T value) używamy gdy mamy pewność, że obiekt T nie będzie null
  • ofNullable(T value) używamy w każdym innym wypadku. Obiekt T może być null

Przykład użycia:

Optional<String> emptyOptional = Optional.empty();

Optional<String> optionalOfValue = Optional.of("javowiec");

Optional<String> emptyOptionalOrOfValue = Optional.ofNullable(nullOrString);

Metoda get

Jednym ze sposobów zwrócenia obiektu opakowanego Optionalem jest wywołanie metody:

public T get()
String resultGet = Optional.of("javowiec").get();

Nie jest to jednak dobry sposób. Metody tej możemy użyć tylko wtedy, gdy jesteśmy całkowicie pewni, że w każdej sytuacji Optional będzie opakowywał obiekt. Jeżeli będzie pusty, wywołanie metody get spowoduje wyrzucenie wyjątku NoSuchElementException. Optional został stworzony z myślą pozbycia się wyjątku NullPointerException, a nie po to by jeden wyjątek zastąpić drugim 😁

Metoda orElse

Lepszym sposobem jest użycie metody:

public T orElse(T other)
String resultOrElse = Optional.ofNullable(test).orElse("default");

Jeżeli Optional nie jest pusty, zwróci opakowywany obiekt. W przeciwnym wypadku zwróci parametr przekazany do metody orElse. Pułapka, o której wspominałem na początku dotyczy właśnie tej metody i już za chwilę wyjaśnię o co chodzi.

Metoda orElseGet

Kolejnym dobrym sposobem jest użycie metody:

public T orElseGet(Supplier<? extends T> supplier)
String resultOrElseGet = Optional.ofNullable(test).orElseGet(Main::getDefault);

Metoda orElseGet działa na podobnej zasadzie co orElse. Jeżeli Optional nie jest pusty, zwróci opakowywany obiekt. W przeciwnym wypadku zwróci wynik wywołania instancji interfejsu funkcyjnego Supplier przekazany do metody orElseGet.

Interfejsy funkcyjne zostały dodane w Javie 8 podobnie jak Optionale. Interfejs funkcyjny Supplier posiada jedną metodę bez parametrów: T get(), która ma za zadanie po prostu zwrócić wynik. Instancje interfejsów funkcyjnych można stworzyć z wykorzystaniem wyrażeń lambda, referencji metod czy referencji konstruktorów:

() -> metoda() // Wyrażenie lambda

Klasa::metoda // Referencja metody

Klasa::new // Referencja konstruktora

Pułapka, czyli orElse vs orElseGet

Jak już pewnie zauważyłeś, orElse oraz orElseGet różnią się parametrem metody. Metoda orElse przyjmuje jako parametr obiekt T, natomiast metoda orElseGet przyjmuje jako parametr interfejs funkcyjny Supplier, a w zasadzie jego instancję.

Nieumiejętne rozróżnienie sytuacji, w których należy użyć jednej z tych metod może doprowadzić do spadku wydajności naszego kodu. Metodę orElse używamy tylko wtedy, gdy posiadamy już stworzony obiekt. Jeżeli w parametrze zawrzemy metodę, która zwraca obiekt, metoda ta zostanie zawsze wykonana. Nawet, jeśli Optional nie jest pusty. Wyobraź sobie, że jeżeli Optional będzie pusty, będziemy chcieli wywołać metodę, która zwróci coś z Bazy Danych. Użycie w tym przypadku metody orElse spowoduje, że zapytanie do Bazy Danych będzie zawsze wykonywane. Bez względu na to, czy Optional jest pusty czy nie!

W sytuacji, gdy Optional może być pusty i chcemy zwrócić obiekt, który jeszcze nie jest gotowy (np. trzeba go ściągnąć z Bazy Danych lub stworzyć), należy użyć metody orElseGet. Metoda ta gwarantuje, że metoda interfejsu funkcyjnego Supplier zostanie wywołana tylko wtedy, gdy Optional jest pusty.

Prostym programem można wykazać tę prawidłowość:

package pl.javowiec;

import java.util.Optional;

public class Main {

    public static void main(String[] args) {
        // Zmienna testowa
        String test = "javowiec";
        System.out.println("Test variable: " + test);

        // Przykład użycia Optional.orElse
        System.out.println("[1] Optional.orElse example");
        String resultOrElse = Optional.ofNullable(test).orElse(getDefault());
        System.out.println("Optional.ofNullable(test).orElse(getDefault()) returns: " + resultOrElse);

        // Przykład użycia Optional.orElseGet
        System.out.println("[2] Optional.orElseGet example");
        String resultOrElseGet = Optional.ofNullable(test).orElseGet(Main::getDefault);
        System.out.println("Optional.ofNullable(test).orElseGet(Main::getDefault) returns: " + resultOrElseGet);
    }

    private static String getDefault() {
        // Zwrócenie domyślnego napisu
        System.out.println("Main.getDefault() method has been invoked");
        return "default";
    }

}

Co robi ten program? Ustawia zmienną testową, następnie opakowuje ją w dwóch Optionalach. Zwracamy obiekt opakowany Optionalami poprzez wywołanie metody orElse oraz orElseGet. Jeżeli zmienna testowa będzie null, to zwrócimy domyślny napis "default" metodą getDefault().

Po uruchomieniu tego programu zauważymy, że linia 14, czyli metoda orElse zwraca opakowany napis "javowiec", ale wywołuje też metodę getDefault(), pomimo że zmienna testowa nie jest null. Metoda getDefault() została wywołana niepotrzebnie.

W przypadku linii 19, czyli metody orElseGet również zostaje zwrócony opakowany napis "javowiec". Jednakże metoda getDefault() nie zostaje wykonana.

Metoda getDefault zawarta w metodzie orElse zostaje wykonana pomimo opakowywania obiektu test przez Optional. Metoda orElseGet w tym wypadku nie wywołuje metody getDefault

Z kolei po zmianie zmiennej testowej na null widzimy, że w obu przypadkach jest wywoływana metoda getDefault() i zwracany domyślny napis "default". Tym razem wykonanie metody getDefault() było konieczne.

Metoda getDefault zawarta w metodzie orElse zostaje wykonana także gdy Optional jest pusty. Metoda orElseGet w tym wypadku również wywołuje metodę getDefault

GitHub

Kod źródłowy jest udostępniony na GitHubie. Poniżej link 🙂

javowiec-pl-java/optional-orElse-vs-orElseGet at main · JavowiecPL/javowiec-pl-java
Gotowe projekty z kategorii Java stworzone na potrzeby postów publikowanych na blogu Javowiec.pl - javowiec-pl-java/optional-orElse-vs-orElseGet at main · JavowiecPL/javowiec-pl-java

Podsumowanie

Zwracaj Optional<T> z metody wtedy, gdy w pewnych okolicznościach nie może zwrócić obiektu T i klient będzie musiał ten przypadek obsłużyć dodatkowym kodem. Kolekcje, mapy, streamy, tablice i inne typy kontenerowe (w tym Optionale) zwracamy puste zamiast opakowywać je w Optionale.

Należy pamiętać, że jeżeli przekażemy null do metody of tworzącej Optional, zostanie wyrzucony wyjątek NullPointerException. Natomiast metoda get wywołana na pustym Optionalu wyrzuci wyjątek NoSuchElementException.

Preferowanymi sposobami zwracania opakowanego obiektu T z Optionala jest użycie metod orElse lub orElseGet. Metodę orElse używamy tylko wtedy, gdy posiadamy już stworzony obiekt. Jeżeli w parametrze zawrzemy metodę, która zwraca obiekt, metoda ta zostanie zawsze wykonana. Nawet, jeśli Optional nie jest pusty. Jeżeli nie posiadamy jeszcze gotowego obiektu (np. trzeba go ściągnąć z Bazy Danych lub stworzyć), należy użyć metody orElseGet. Metoda ta gwarantuje, że metoda interfejsu funkcyjnego Supplier zostanie wywołana tylko wtedy, gdy Optional jest pusty.

Liczba komentarzy:

Zaloguj się lub dołącz do społeczności Javowców, aby móc uczestniczyć w dyskusji 🙂