Menu

Optional w Javie: jak unikać null i NullPointerException

Czym jest java.util.Optional, jak go utworzyć i jak bezpiecznie odczytać jego wartość przez map, filter, orElse i ifPresent zamiast sprawdzać null.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Problem, który rozwiązuje Optional

Metoda, która może nie mieć odpowiedzi, zawsze stawiała w Javie niewygodny wybór: zwrócić null i liczyć na to, że wywołujący pamięta o sprawdzeniu. Zwykle nie pamięta, a skutkiem jest NullPointerException, który pojawia się daleko od metody, która zwróciła null.

Optional<T> to małe pudełko, które albo zawiera wartość, albo jest jawnie puste. Zwracając Optional<User> zamiast User, metoda mówi wywołującym w swojej sygnaturze: "mogę nic nie znaleźć", a kompilator popycha ich do obsłużenia tego przypadku.

Tworzenie Optional

Są trzy metody fabrykujące i wybór właściwej ma znaczenie:

  • Optional.of(value): wartość nie może być null, w przeciwnym razie od razu rzuca NullPointerException.
  • Optional.ofNullable(value): pusty, jeśli wartość to null, w przeciwnym razie z wartością. Używaj go do opakowania czegoś, co może być null.
  • Optional.empty(): pusty Optional.

Częsty błąd to sięgnięcie po Optional.of(x) dla wartości, która może być null. To niweczy cały sens i rzuca dokładnie ten wyjątek, którego chcesz uniknąć. W razie wątpliwości użyj ofNullable.

Bezpieczny odczyt wartości

Gdy masz już Optional, celem jest wyciągnięcie wartości bez zakładania, że tam jest. Toporne narzędzie to get() i prawie nigdy nie należy go używać:

Optional<String> name = Optional.empty();
String value = name.get();   // rzuca NoSuchElementException: to tylko przebrany NPE

Zamiast tego podaj wartość zapasową albo zareaguj na obecność wartości. orElse zwraca wartość domyślną, gdy Optional jest pusty; ifPresent uruchamia kod tylko wtedy, gdy wartość istnieje:

Użyj orElseGet(supplier) zamiast orElse, gdy zbudowanie wartości domyślnej jest kosztowne: supplier uruchamia się tylko wtedy, gdy Optional rzeczywiście jest pusty. Argument orElse jest obliczany zawsze, nawet gdy nie jest potrzebny.

orElseThrow, gdy "to powinno istnieć"

Czasem brak wartości naprawdę jest błędem: wymagana wartość konfiguracji albo użytkownik, który powinien być w bazie danych. orElseThrow zamienia pusty Optional w jasny wyjątek wybranego przez ciebie typu:

Czyta się to dużo lepiej niż blok if (opt.isPresent()) i pokazuje błąd wprost w miejscu, w którym występuje.

Przekształcanie przez map i filter

Prawdziwa korzyść to łączenie w łańcuchy. map stosuje funkcję do wartości tylko wtedy, gdy ona istnieje, a pusty Optional zostawia pustym, więc przekształcasz dane, nigdy nie dotykając null. filter odrzuca wartość, jeśli nie przejdzie testu.

Jeśli funkcja mapująca sama zwraca Optional, użyj flatMap zamiast map, żeby uniknąć niezgrabnego Optional<Optional<T>>. To odpowiada rozróżnieniu map/flatMap znanemu ze strumieni: Optional zachowuje się podobnie jak strumień zawierający zero albo jeden element.

Gdzie Optional pasuje (a gdzie nie)

Optional zaprojektowano jako typ zwracany przez metody, które zgodnie z zasadami mogą nie dać wyniku. Klasyczne przykłady to Stream.findFirst(), wyszukiwania w stylu Map i parsowanie. Używaj go tam, a twoje API samo będzie dokumentować swoje luki.

Nie jest przeznaczony do:

  • Pól. Nie jest serializowalny i dodaje osobny obiekt na każde pole. Używaj zwykłych referencji i sprawdzaj je w konstruktorze.
  • Parametrów metod. Wywołujący musieliby wtedy opakowywać argumenty w Optional.of(...), co jest bardziej uciążliwe niż przeciążenie albo parametr dopuszczający null.
  • Kolekcji. Zwracaj pustą List, a nie Optional<List>. Pusta kolekcja już oznacza "nic".

Traktowany jako typ zwracany, Optional zamienia ciche błędy z null w podpowiedzi na etapie kompilacji, by obsłużyć brakujący przypadek.

Dalej: wyjątki

Optional zgrabnie obsługuje codzienny przypadek "wartości może nie być", ale niektóre błędy są naprawdę wyjątkowe: plik, którego nie da się otworzyć, zerwane połączenie sieciowe, dane, których nie da się sparsować. Java modeluje je wyjątkami i o nich jest następna strona.

Najczęściej zadawane pytania

Czym jest Optional w Javie i po co go używać?

Optional<T> to kontener, który albo zawiera wartość, albo jest pusty. Istnieje po to, by "wartości może nie być" było widać wprost w typie zwracanym metody. Dzięki temu wywołujący są skłaniani do obsłużenia pustego przypadku, zamiast zapomnieć o sprawdzeniu null i dostać NullPointerException w czasie działania. Używaj go głównie jako typu zwracanego przez metody, które mogą nie dać wyniku, np. wyszukiwanie, które nic nie znajduje.

Czym różni się Optional.of od Optional.ofNullable?

Optional.of(x) od razu rzuca NullPointerException, jeśli x to null; używaj go, gdy wiesz, że wartość nie jest null. Optional.ofNullable(x) zwraca pusty Optional, gdy x to null, a w przeciwnym razie Optional z wartością; używaj go, gdy wartość może być null. Optional.empty() daje bezpośrednio pusty Optional.

Czy do odczytu wartości używać Optional.get()?

Unikaj gołego get(): rzuca NoSuchElementException, gdy Optional jest pusty, czyli to po prostu NullPointerException pod inną nazwą. Wybieraj orElse, orElseGet, orElseThrow, ifPresent albo map, żeby pusty przypadek zawsze był obsłużony. Jeśli musisz najpierw sprawdzić, zabezpiecz się przez isPresent(), ale metody funkcyjne są czytelniejsze.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ