Optional w Javie: jak używać go poprawnie i czego unikać
Klasa Optional, dostępna w Javie od wersji 8, miała pomóc w walce z wyjątkiem NullPointerException. Dobrze użyta robi kod czytelniejszym, ale w praktyce widuje się ją w miejscach, w których tylko zaciemnia obraz. Ten wpis pokazuje, do czego Optional został stworzony, jak z niego korzystać i jakich pułapek unikać.
Po co powstał Optional
Metody, które mogą nie zwrócić wyniku, od zawsze zwracały null. Problem w tym, że sygnatura metody nie mówi wprost, czy wolno się go spodziewać. Programista, który zapomni o sprawdzeniu, dostaje błąd w czasie działania programu. Optional ma zakomunikować w typie zwracanym: ta wartość może nie istnieć, więc musisz to obsłużyć.
Najlepiej sprawdza się jako typ zwracany przez metody, które czasem nie mają sensownego wyniku, na przykład wyszukiwanie po identyfikatorze:
Optional<User> findById(long id) {
return Optional.ofNullable(repository.get(id));
}
Wywołujący od razu widzi, że użytkownika może nie być, i musi zdecydować, co wtedy zrobić.
Tworzenie i odczytywanie wartości
Do utworzenia obiektu służą trzy metody fabryczne:
Optional.of(wartosc)tworzy Optional z wartością, która nie może być nullem; w przeciwnym razie zgłasza wyjątek,Optional.ofNullable(wartosc)akceptuje null i zwraca pusty Optional,Optional.empty()zwraca pusty obiekt.
Do odczytu wartości lepiej unikać metody get() bez wcześniejszego sprawdzenia, ponieważ w razie braku wartości rzuci wyjątek, co niczym nie różni się od klasycznego błędu z nullem. Zamiast tego używaj bezpieczniejszych odpowiedników:
String imie = findById(5)
.map(User::getName)
.orElse("nieznany");
User user = findById(5)
.orElseThrow(() -> new IllegalArgumentException("Brak użytkownika"));
Metoda orElse zwraca wartość domyślną, a orElseThrow pozwala wyrazić, że brak wartości to sytuacja błędna. Od Javy 10 dostępna jest też bezargumentowa wersja orElseThrow().
Przetwarzanie w stylu funkcyjnym
Siła Optionala tkwi w łączeniu operacji bez zagnieżdżonych instrukcji if. Metoda map przekształca wartość, jeśli istnieje, filter zostawia ją tylko wtedy, gdy spełnia warunek, a ifPresent wykonuje akcję tylko dla istniejącej wartości:
findById(5)
.filter(User::isActive)
.ifPresent(u -> sendWelcome(u));
Gdy trzeba obsłużyć obie sytuacje, od Javy 9 możesz użyć ifPresentOrElse. Jeśli funkcja przekształcająca sama zwraca Optional, wybierz flatMap, żeby uniknąć konstrukcji Optional<Optional<T>>.
Częste błędy
Optional nie jest remedium na wszystko. Najczęściej powtarzające się błędy to:
- Optional jako pole klasy. Nie został zaprojektowany do przechowywania stanu, nie jest serializowalny i zwiększa zużycie pamięci. Pole lepiej zostawić jako zwykłe, a Optional zwracać z gettera, jeśli to ma sens.
- Optional jako parametr metody. Zmusza wywołującego do opakowywania wartości i nie zapobiega przekazaniu nulla. Prostszym rozwiązaniem bywa przeciążenie metody.
- Zwracanie nulla zamiast Optionala. Metoda, która deklaruje zwrot Optionala, nigdy nie powinna zwracać
null. - Użycie
isPresent()razem zget(). To tylko stary kod z nullem w nowym opakowaniu. Zamiast tego użyjmap,orElselubifPresent. - Optional dla kolekcji. Zamiast
Optional<List<T>>zwracaj po prostu pustą listę.
Wydajność i kontekst użycia
Każdy Optional to dodatkowy obiekt, więc w gorących pętlach, przetwarzających miliony elementów, bywa to odczuwalne. W typowych aplikacjach biznesowych koszt jest pomijalny, ale warto o nim pamiętać przy kodzie krytycznym wydajnościowo. Dla typów prostych istnieją wyspecjalizowane warianty: OptionalInt, OptionalLong i OptionalDouble, które unikają opakowywania liczb.
Optional dobrze współpracuje ze strumieniami. Metody takie jak findFirst() czy max() zwracają Optional, dzięki czemu można wygodnie obsłużyć przypadek pustego strumienia:
String najdluzszy = slowa.stream()
.max(Comparator.comparingInt(String::length))
.orElse("");
Podsumowanie
Optional to narzędzie do jednego głównego celu: jasnego komunikowania, że wynik metody może być nieobecny. Używaj go w typach zwracanych, łącz operacje przez map, filter i orElse, a unikaj w polach, parametrach i kolekcjach. Jeśli w projekcie masz wiele zagnieżdżonych sprawdzeń nulla, spróbuj zmienić jedną metodę wyszukującą na zwracającą Optional i zobacz, jak zmienia się czytelność kodu wokół niej.
Na koniec praktyczna uwaga dotycząca testów. Metody zwracające Optional są łatwe do sprawdzenia: wystarczy napisać jeden test dla przypadku z wartością i jeden dla przypadku pustego wyniku. Dzięki temu obie ścieżki są jawnie pokryte, a zmiana kontraktu metody, na przykład z zawsze obecnej wartości na opcjonalną, od razu wymusi poprawki w miejscach użycia. To jeden z cichych, ale realnych zysków z tego podejścia w większych projektach, nad którymi pracuje wiele osób.