Optional i Java – nie tylko isPresent i get

Czym jest Optional?

Wraz ze wszystkimi dobrodziejstwami Javy 8 dotarł do nas długo oczekiwany Optional – czyli wrapper na inny obiekt, który informuje nas czy obiekt ów tam się znajduje, czy może jednak nie. W najogólniejszym opisie miał on służyć do zastąpienia zwracania null pustym Optional właśnie – i tak przykładowa metoda find(employee.getId()) powinna nam zwracać nie obiekt o danym id bądź null, lecz obiekt typu Optional, w którym może się znajdować dany obiekt, bądź pustego Optionala, gdy nie znajdzie danego obiektu, dzięki temu unikniemy uwielbianego przez wszystkich NullPointerException.

Przejście z null do Optional


Jeśli chcesz prześledzić zmiany krok po kroku w formie wideo, to zapraszamy na nasz kanał na YouTube.


Programistyczna brać radośnie zaczęła go stosować i codebase na całym świecie zalała fala

Optional<Employee> employee = employeeRepository.find("Pawel");

if(employee.isPresent()) {
    employee.get().giveRiseTo();
}

będąca odpowiednikiem

Employee employee = employeeRepository.find("Pawel");

if(employee != null) {
    employee.giveRiseTo();
}

W przypadku wersji z Optional mamy trochę więcej kodu, jednak już przez samo jego użycie w ten prosty sposób mamy zysk – korzystający z funkcji zwracającej obiekt opcjonalny wprost informujemy świat, że obiektu może tam nie być i klient musi sobie jakoś z tym poradzić. No i nie natknie się on na „null-niespodziankę”.
Jednak Optional to nie tylko rozbudowana wersja if’a.

Tworzenie

Zacznijmy jednak od początku – czyli jak utworzyć obiekt tego typu.

Opcje mamy aż trzy – metody statyczne klasy Optional .of(), .ofNullable() i .empty().

  • .of() zwraca Optional z obiektem podanym jako argument,
  • .ofNullable() robi to samo co of, z tą różnicą, że jeśli przesłany jako argument metody obiekt jest nullem, to zwraca ona pustego Optionala,
  • .empty() zwraca Optionala pustego.

Tutaj też pojawia się pewien haczyk – jeśli do .of() prześlemy obiekt będący nullem… to dostaniemy optionala wypełnionego, nie pustego, ale obiektem, po rozpakowaniu będzie… null! Więc właśnie zastawiliśmy śliczną pułapkę na przyszłe pokolenia programistów, które odziedziczą nasz kod.

Przejdźmy do kodu, stwórzmy repozytorium pracowników z metodą find, która zwraca pracownika na podstawie imienia, opakowanego w Optional

public class EmployeeRepository {

    private static Map<String, Employee> employees = new HashMap<>();

    static {
        employees.put("Pawel", new Employee("Pawel", 30));
    }

    private EmployeeRepository() { }

    public static Optional<Employee> find(String name) {
        return Optional.ofNullable(employees.get(name));
    } 
}

Klasa Employee jest prostym obiektem domenowym

public class Employee {

 private String name;
 private int age;

 public Employee(String name, int age) {
     this.name = name;
     this.age = age;
 }

 public String getName() {
     return this.name;
 }
 
 public int getAge() {
     return this.age;
 }
}

I teraz w głównej klasie aplikacji możemy pobrać optionala…

public class App {

    public static void main(String[] args) {
        Optional<Employee> employee = EmployeeRepository.find("Pawel");
    }
}

Podstawy

Mamy już nasz obiekt, możemy go więc wykorzystać. Takim podstawowym sposobem jest formułka – .isPresent() -> .get()

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");

    if(employee.isPresent()) {
        Employee emp = employee.get();
        System.out.println(emp.getName());
    }
}

Co wypisze nam imie pracownika.

isPresent() zwróci nam boolean w zależności od tego, czy Optional jest pusty, czy nie. Mamy do dyspozycji również jego lustrzane odbicie – .isEmpty(). Używamy powyżej też metody .get(), która zwróci nam obiekt opakowany w Optional. Należy tu się mieć na baczności, ponieważ jeśli wywołamy na pustym obiekcie .get() zostanie rzucony nam runtime exception, stąd też zawsze należy pamiętać o poprzedzeniu wywołania tej metody sprawdzeniem, czy Optional na pewno jest wypełniony.

Rzeczy ciekawsze

Tyle z popularnych podstaw, czas przejść do rzeczy ciekawszych. Po pierwsze powyższe możemy zapisać za pomocą jednej linijki.

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");

    employee.ifPresent(emp -> System.out.println(emp.getName()));
}

Metoda ifPresent() przyjmuje jako argument interfejs funkcyjny Consumer o typie takim samym jak typ Optionala. Consumer posiada jedna metodę – accept, która przyjmuje obiekt i nie zwraca nic. Czyli krótko mówiąc bierze to co jest w Optionalu i coś z tym robi. Jeśli Optional jest pusty to nic się nie dzieje.

Dodatkowo do dyspozycji mamy znany i lubiane z pracy ze strumieniami metody .filter() i .map(), które w wersji dla Optionala nie zwracają strumienia, lecz Optional danego typu. Na przykład:

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");

    Optional<String> employeeName = employee.map(Employee::getName);

    employeeName.map(String::toUpperCase).ifPresent(System.out::println);
}

Co robi map to zmienia nam jeden typ obiektu na drugi (korzysta wewnątrz z interfejsu funkcyjnego Function). W naszym wypadku po przemapowaniu pracownika na jego imie dostajemy z Optional<Employee>Optional<String>, który następnie możemy ponownie zmapować, by imie pracownika wypisać finalnie wielkimi literami. Dodatkowo umożliwiło nam to korzystanie z referencji do metod. Oczywiście całość możemy (a nawet powinniśmy) zapisać w jednym wyrażeniu.

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");

    employee
        .map(Employee::getName)
        .map(String::toUpperCase)
        .ifPresent(System.out::println);
}

Metoda .filter() sprawdza, czy obiekt w naszym optionalu spełnia dany warunek, możemy rozbudować nasz łańcuch wywołań o wypisanie imienia wielką literą, tylko jeśli nasz pracownik ma ukończone 30 lat.

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");

    employee.
        filter(emp -> emp.getAge()>=30)
        .map(Employee::getName)
        .map(String::toUpperCase)
        .ifPresent(System.out::println);
}

Filter przyjmuje jako argument interfejs Predicate, który bierze obiekt o danym typie i zwraca nam boolean – czyli true bądź false.

Pozytywną ścieżkę mamy opanowaną, a co gdy dostaniemy pusty Optional, w którymś momencie… korzystać z .isPresent()?

Gdy Optional jest pusty

Na szczęście nie ma takiej potrzeby, ponieważ mamy do dyspozycji metody .orElse(), .orElseGet() oraz .orElseThrow(). Pierwsze dwa z nich są bardzo podobne – jako argument podajemy „domyślną” wartość, gdy Optional będzie pusty. Różnica jest taka, że .orElse() przyjmuje obiekt wprost, natomiast .orElseGet obiekt typu Supplier. Trzecia z metod rzuca po prostu wyjątkiem.

Gdybyśmy chcieli rzucić wyjątkiem jeśli nie znajdziemy pracownika o danym imieniu możemy zrobić to tak:

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Jacek");

    employee
        .filter(emp -> emp.getAge()>=30)
        .map(Employee::getName)
        .map(String::toUpperCase)
        .orElseThrow(()-> new RuntimeException("Brak odpowiedniego pracownika"));
}

Zmieniliśmy imię pracownika na nieistniejącego – powyższy program rzuci nam RuntimeExcepion. Dużym minusem orElsów jest to, że zwracają nam zawsze obiekt jeśli jednak taki istnieje i wszystko się powiedzie. Z tego wynika, że nie możemy użyć .map().ifPresent().orElseThrow(), bądź na przykład w trakcie wykonywania łańcucha wstrzyknąć domyślnej wartości i na niej wykonywać pozostałe operacje.

Dlatego też jeśli chcemy wpisać imie, które otrzymamy musimy pobrać wynikowego Stringa z optionala i dopisać dodatkowa linijkę.

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Pawel");
    String employeeName = employee
            .filter(emp -> emp.getAge() >= 30)
            .map(Employee::getName)
            .map(String::toUpperCase)
            .orElseThrow(() -> new RuntimeException("Brak odpowiedniego pracownika"));
    System.out.println(employeeName);
}

Dzięki temu zyskujemy pewność, że ten String na pewno istnieje i nie musimy robić nullchecków.

Java 9

W Javie 9 pojawiły sie trzy nowe metody – .or(), .ifPresentOrElse() i .stream().

Ostatnia z nich jest dość „prosta” – zmienia obiekt opakowany w Optional (jeśli istnieje) w stream, a jeśli nie istnieje, to otrzymujemy pusty strumień. Pozostałe dwie są dla nas o wiele ciekawsze.

.or() wypełnia lukę, o jakiej pisałem powyżej – pozwala przesłać domyślą wartość jeśli w którymś momencie dostaniemy pustego Optionala i kontynuować wykonywanie operacji na niej:

public static void main(String[] args) {
        Optional<Employee> employee = EmployeeRepository.find("Jacek");

        employee
            .or(() -> Optional.of(new Employee("Krzysztof", 40)))
            .filter(emp -> emp.getAge() >= 30)
            .map(Employee::getName)
            .map(String::toUpperCase).ifPresent(System.out::println);

    }

Dzięki użyciu .or() przesyłamy do łańcucha wywołań pracownika o imieniu „Krzysztof” jeśli nie znajdziemy pracownika o imieniu „Jacek”.

.ifPresentOrElse() natomiast przyjmuje implementacje dwóch interfejsów  – Consumer oraz Runnable. Pierwszy jest odpalany jak standardowy .ifPresent(), natomiast czemu drugim jest Runnable, kojarzony z wątkami? Jest on interfejsem funkcyjnym z jedną metodą (run() ), która nie przyjmuje żadnych argumentów oraz nic nie zwraca. W przypadku .ifPresentOrElse() argument typu runnable jest odpalany, gdy Optional jest pusty – czyli nie mamy nic co możemy przesłać jako argument (a standardowy Consumer tego wymaga). Czyli Runnable ze swoja metoda void run() idealnie się wpasował w potrzebę.

public static void main(String[] args) {
    Optional<Employee> employee = EmployeeRepository.find("Jacek");

    employee
            .filter(emp -> emp.getAge() >= 30)
            .map(Employee::getName)
            .map(String::toUpperCase)
            .ifPresentOrElse(
                    employeeName -> System.out.println(employeeName),
                    () -> System.out.println("Nie znaleziono pracownika")
            );
}

Powyższy kawałek kodu wypiszę nam imie pracownika (wielkimi literami), jeśli dany pracownik istnieje, jeśli nie to na standardowe wyjście dostaniemy informację o braku pracownika.

Podsumowanie

To tyle, jeśli chodzi o użycie klasy Optional z mojej strony. Mam nadzieje, że po przeczytaniu tego artykułu, następnym razem, gdy przyjdzie Tobie z nim pracować (bo już na stałe zagościły w Javie) będziesz pamiętać o .map(), .ifPresent() i pozostałych, dzięki czemu zaoszczędzisz trochę pisania i Twój kod będzie czytelniejszy.

Kod aplikacji dostępny jest na githubie.

Jeśli ten artykuł przypadł Ci do gustu to zapraszam do zasubskrybowania bloga i dostawania na bieżąco informacji o nowych wpisach.


Podziel się tym wpisem:

1 komentarz do wpisu “Optional i Java – nie tylko isPresent i get

Dodaj komentarz