Java Optional
Każdy programista Javy przynajmniej raz w życiu spotkał się z najpopularniejszym wyjątkiem NullPointerException. Jest to jeden z najczęstszych błędów, który potrafi skutecznie utrudnić debugowanie i powodować nieprzewidziane błędy w aplikacji.
Właśnie dlatego w Javie 8 pojawiła się klasa Optional (wszystkie nowości m.in. z Java 8 opisywałem tutaj), która pozwala na bezpieczne operowanie wartościami, które mogą być null. Dzięki niej możemy uniknąć wielu błędów i sprawić, iż nasz kod stanie się bardziej czytelny i odporny na błędy.
Czym jest Optional w Javie
Optional<T> to klasa opakowująca, która może przechowywać wartość typu T lub być pusty. Zamiast zwracać null, możemy zwrócić Optional.empty() albo Optional.of(value), co zmusza programistę metody do świadomego obsłużenia przypadku braku wartości.
Zamiast takiego kodu:
public String getName(User user) { return user != null ? user.getName() : "Anonim"; }Możemy użyć Optional:
public Optional<String> getName(User user) { return Optional.ofNullable(user).map(User::getName); }Dzięki temu eliminujemy ryzyko NullPointerException i wymuszamy do obsługi pustej wartości.
Jak poprawnie używać Java Optional?
Klasa Optional daje wiele możliwości, ale aby w pełni wykorzystać jej potencjał, warto znać dobre praktyki i unikać typowych błędów.
Tworzenie instancji Optional
// Tworzy Optional z wartością Optional<String> optional1 = Optional.of("Hello"); // Może być pusty lub zawierać wartość Optional<String> optional2 = Optional.ofNullable(null); // Zawsze pusty Optional Optional<String> optional3 = Optional.empty();of() – używamy, gdy mamy pewność, iż wartość nie jest null.
ofNullable() – pozwala na obsługę null.
empty() – zwraca pusty Optional.
Pobieranie wartości z Optional
// Może rzucić NoSuchElementException String value1 = optional1.get(); // Zwraca wartość lub domyślną String value2 = optional2.orElse("Domyślna wartość"); // Lazy loading String value3 = optional2.orElseGet(() -> "Wartość z dostawcy"); String value4 = optional2.orElseThrow(() -> new IllegalStateException("Brak wartości!"));W przypadku użycia metody get() musimy być pewni, iż nie dostaniemy pustej wartości, w przeciwnym wypadku możemy dostać wyjątek NoSuchElementException. jeżeli nie jesteśmy pewni, iż wartość zwróci odpowiedni wynik, to możemy dodać wstępną walidację albo użyć innej, bardziej dedykowanej metody: orElse(), orElseGet(), orElseThrow().
Operacje na Optional
Optional oferuje szereg metod, które pozwalają na bezpieczną manipulację wartościami, bez konieczności sprawdzania wartości null. Dzięki nim możemy unikać potencjalnych błędów i pisać bardziej idiomatyczny kod. Poniżej przedstawiam najważniejsze operacje, które warto znać.
Sprawdzanie obecności wartości isPresent(), isEmpty()
Czasami chcemy po prostu sprawdzić, czy Optional zawiera wartość.
Optional<String> optional = Optional.of("Java"); if (optional.isPresent()) { System.out.println("Optional zawiera wartość: " + optional.get()); }Od Javy 11 możemy użyć isEmpty(), które zwraca true, gdy Optional jest pusty.
if (optional.isEmpty()) { System.out.println("Brak wartości"); }Wykonywanie operacji na wartości (ifPresent(), ifPresentOrElse())
ifPresent() pozwala na wykonanie akcji tylko wtedy, gdy Optional zawiera wartość.
optional.ifPresent(value -> System.out.println("Wartość: " + value));Od Javy 9 dostępna jest również metoda ifPresentOrElse(), która pozwala obsłużyć oba przypadki – gdy wartość jest obecna oraz gdy Optional jest pusty.
optional.ifPresentOrElse( value -> System.out.println("Wartość: " + value), () -> System.out.println("Brak wartości") );Jest to bardziej elegancka alternatywa dla isPresent() i isEmpty().
Transformacja wartości map()
Jeśli chcemy przekształcić wartość w Optional, zamiast sprawdzania i manualnego pobierania wartości, możemy użyć map().
Optional<String> optional = Optional.of("Java"); Optional<Integer> length = optional.map(String::length); length.ifPresent(System.out::println); // OUTPUT: 4Łączenie Optional (or())
Od Javy 9 możemy użyć or(), aby dostarczyć alternatywny Optional.
Optional<String> result = optional.or(() -> Optional.of("Domyślna wartość")); System.out.println(result.get()); // Java (jeśli `optional` zawiera wartość)Pułapki, których należy unikać
Mimo, iż Optional wydaje się dobrym pomysłem na używanie go zawsze, to są jednak miejsca w których jednak nie powinno się go stosować
Nie używaj Java Optional jako argumentu metody
Przekazywanie Optional jako argumentu metody jest uważane za złą praktykę. Powoduje to niepotrzebne komplikacje i wymusza na programiście metody obsługę Optional, zamiast stosować prostsze podejście.
// Tak nie rób !!! public void processUser(Optional<User> user) { if (user.isPresent()) { System.out.println("Przetwarzanie użytkownika: " + user.get().getName()); } } // Lepiej zrób tak !!! public void processUser(User user) { Optional.ofNullable(user).ifPresent(u -> System.out.println("Przetwarzanie użytkownika: " + u.getName())); } // Przekazujemy w tej sytuacji obiekt, a dopiero wewnątrz metody go obsługujemyNie używaj Optional jako pola w encjach JPA
Optional nie jest wspierane w encjach JPA i jego użycie może prowadzić do problemów z serializacją oraz ORM.
Nie używaj Optional tam, gdzie nie jest potrzebny
Niektóre przypadki użycia Optional są zbędne i tylko komplikują kod. Przykładowo, w kodzie wewnętrznym klasy (np. przy polach prywatnych), Optional często nie jest konieczny.
// Tak nie rób !!! public class User { private Optional<String> phoneNumber = Optional.empty(); // Niepotrzebne! public Optional<String> getPhoneNumber() { return phoneNumber; } } // Lepiej zrób tak !!! public class User { private String phoneNumber; // Można pozostawić jako nullable public Optional<String> getPhoneNumber() { return Optional.ofNullable(phoneNumber); } } // Nie ma potrzeby przechowywania Optional jako pola w klasie – wystarczy go zwrócić w getterzeUważaj na nadmierne stosowanie Java Optional
Niektórzy programiści próbują używać Optional wszędzie, co prowadzi do zbędnego narzutu. Najlepiej stosować go tylko tam, gdzie naprawdę rozwiązuje problem null.
Przykłady, gdzie Optional nie jest potrzebny:
- W prywatnych metodach, gdzie null może być łatwo obsłużony.
- W strukturach danych (np. listach, mapach).
- Jako pola w modelach danych.
Przykład listy:
List<Optional<User>> users = new ArrayList<>(); // Użycie powyższego jest niepotrzebne // Można podejść klasycznie do tego, nie robić optional na poziomie kolekcji a w samej metodzie wyszukującej List<User> users = new ArrayList<>();Java Optional podczas pobierania danych z bazy
Jednym z najlepszych zastosowań Optional jest obsługa metod, które pobierają dane z bazy danych.
Spring Data JPA domyślnie wspiera Optional przy metodach wyszukujących.
@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByEmail(String email); }Wymusza to na programiście obsłużyć przypadek, gdy użytkownik o podanym e-mailu nie istnieje. Przykład użycia:
Optional<User> userOptional = userRepository.findByEmail("test@example.com"); userOptional.ifPresent(user -> System.out.println("Znaleziono użytkownika: " + user.getName()));Podsumowanie
Optional to potężne narzędzie, które pozwala unikać problemów z null i sprawia, iż kod jest czytelniejszy i bardziej bezpieczny. Warto stosować Optional tam, gdzie zwracanie null może prowadzić do błędów, ale jednocześnie nie należy go nadużywać – na przykład w polach encji JPA, na wyjściu z API lub jako argumenty metod.
W skrócie: jeżeli metoda może zwrócić brakującą wartość, użycie Optional jest bardzo dobrą praktyką!