Czym jest Optional?
Jednym z najczęstszych zarzutów do języka Javy jest to, że nie jest to język, które w łatwy sposób radzi sobie z brakiem przypisanej referencji do obiektu (czyli po prostu z null pointerem). Częstość z jaką programiści Javy musieli radzić sobie z nieprzewidywanymi wyjątkami typu null pointer (ang. null pointer exception), że w końcu sami twórcy języka zaproponowali rozwiązanie. Klasa opakowująca Optional nie jest jednak zwykłym null checkiem.
W czasach przed JDK 8, jedynym sposobem obsługi nullpointera był kod tego typu:
if (person != null) { if (person.getAddress() != null) {
Nie jest on zbyt elegancki. W JDK 8 z pomocą przychodzi Optional, który opakowuje obiekt typu T lub jego brak. Sama klasa zawiera wiele interesujących metod, dzięki którym można zdefiniować zachowanie obiektu, który klasa Optional opakowuje. Można ją wykorzystać na kilka sposobów. Oto one.
Tworzenie typu Optional
Pierwszą opcją jest stworzenie pustej instancji:
Optional<String> empty = Optional.empty();
Powyższy kod informuje o tym, że klasa String nie zawiera wartości. Do sprawdzenia czy wartość jest obecna w typie opakowującym, służy metoda isPresent. Kolejną możliwością jest stworzenie „optionala”, który nie powinien być nullem.
String value = "test"; Optional<String> optionalValue = Optional.of(value);
W przypadku, gdy zmienna value będzie nullem, Optional wyrzuci wyjątek podobny do poniższego:
Exception in thread "main" java.lang.NullPointerException at java.util.Objects.requireNonNull(Objects.java:203) at java.util.Optional.<init>(Optional.java:96) at java.util.Optional.of(Optional.java:108) at java8.optional.ReceiptMain.main(ReceiptMain.java:22)
Połączeniem obu powyższych opcji jest metoda ofNullable, która w przypadku nulla wyrzuci pusty optional.
Optional<String> ofNullable = Optional.ofNullable(test);
Metoda ofNullable
public class Receipt { private Integer id; private String name; private String address; public Receipt() { } public Receipt(Integer id, String name, String address) { this.id = id; this.name = name; this.address = address; } public Integer getId() { return Optional.ofNullable(id).isPresent() ? id : 0; } public String getAddress() { return Optional.ofNullable(address).orElse("default address"); } public Optional<String> getName() { return Optional.ofNullable(name); } }
Jest to zwykła klasa POJO. Jedyną różnicą jest skorzystanie z klasy Optional w getterze. Jest wiele różnych szkół w jaki sposób używać klasy Optional. W powyższym przykładzie zademonstrowałem trzy z nich. Jedna opcja, to opakowanie zmiennej w Optional i sprawdzenie warunekiem isPresent, czy wartość w zmiennej istnieje czy też jest nullem. Inną (preferowaną opcją) jest skorzystanie z metody orElse, która ustawia wartość domyślną w przypadku, gdy pojawi się null. Ostatnią opcją jest zwracanie po prostu całego optionala, co z w przypadku wystąpienia null pointera, wyrzuci wartość Optional.empty.
Pozostałe metody klasy Optional
Optional posiada wiele innych przydatnych metod. Część z nich powiela się z tymi, które będę omawiał przy lekcji odnośnie Stream API*. Dlatego teraz skupię się na kilku typowych dla optionali. Pierwszą z nich jest orElseThrow, która działa podobnie jak orElse, z tą różnicą, że wyrzuca dowolny wyjątek zawarty w lambdzie.
public Integer getTaxId() { return Optional.ofNullable(taxId).orElseThrow(() -> new RuntimeException("Tax id is required for the receipt.")); }
Możliwe jest też wywołanie funkcji w przypadku, gdy zajdzie wartość nullowa. Do tego z pomocą przyjdzie metoda orElseGet.
public String getAddress() { return Optional.ofNullable(address).orElseGet(() -> "default"); }
Ostatnią ciekawą metodą jest ifPresent, która może być przydatna, jeśli chcesz skorzystać z lambdy w momencie, gdy wartość jest obecna (np. do wyświetlenia pomocniczych logów).
optional.ifPresent(value -> { System.out.println("Value found - " + value); });
Problematyczna metoda get
Na koniec pozostawiłem metodę get. Metoda ta zwraca wartość typu, który został uprzednio opakowany, jeśli nie jest on nullem, w przeciwnym przypadku wyrzuci wyjątek NoSuchElementException.
Serializacja
Uważaj na opakowanie pól w Optional w swoich klasach, które później będą podlegały procesowi serializacji**. Niestety mechanizm optionali w JDK 8 nie pozwala na ich serializację, co może spowodować wiele nieumyślnych błędów w Twoim kodzie. Wynika to z tego, że eksperci z grupy Java Lambda (JSR-335) odrzucili tą opcję. Intencją klasy jest, aby wywołujący natychmiast sprawdził Optional i wyodrębnił rzeczywistą wartość, jeśli jest obecna (głównie przy korzystaniu z biblioteki Stream AP). Nigdy jego celem nie było nakładanie go na obiekty, będące modelem danych.
*Czym jest Stream API: https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
**Czym jest serializacja, znajdziesz w lekcji o JAXB.
Kod z lekcji: https://github.com/developeronthego/java-jdk8/tree/master/src/main/java/java8/optional