Jeśli programujesz w Javie, to na pewno kojarzysz wyjątek podany w tytule. Mimo że jego naprawa zwykle jest prosta, to kluczem do sukcesu jest uniknięcie wyjątku, zanim on nastąpi.
Z pozoru prosta implementacja
Załóżmy, że mamy dane id użytkownika, następnie chcemy go pobrać (z jakiejś bazy / repozytorium użytkowników) i wyświetlić w konsoli jego nazwę. Brzmi banalnie? Pierwsza implementacja mogłaby wyglądać w ten sposób:
1User user = findUser(userId); 2 System.out.println(user.getName());
Gdzie metoda findUser wyglądałaby tak:
1public User findUser() { 2 return users.get(userId); 3}
Na pierwszy rzut oka, wszystko wygląda w porządku. Jednak co w przypadku, gdy dostaniemy id, którego nie ma w bazie? Cóż, ponieważ metoda findUser musi zwrócić obiekt typu User, więc zwróci null. Zapewne stwierdzicie, że wystarczy dodać proste sprawdzenie:
1User user = findUser(userId); 2 if (user != null) { 3 System.out.println(user.getName()); 4 }
Faktycznie, załatwi ono sprawę. Jednak zastanówmy się... dlaczego dodaliśmy to sprawdzenie? Stało się tak, ponieważ otrzymaliśmy NullPointerException. Tylko że wolelibyśmy w ogóle uniknąć jego wystąpienia. Możemy spróbować dodać sprawdzenie dla każdej możliwej zmiennej w projekcie, jednak wówczas takie sprawdzenia stanowiłyby połowę naszego kodu, a tego przecież nie chcemy.
Optional
Z pomocą przychodzi Optional, klasa z nie tak nowej już zresztą Javy 8. Jest to swego rodzaju „opakowanie” dla zmiennej, która może (ale nie musi) być nullem. Zmieńmy zatem implementację metody findUser:
1 public Optional<User> findUser(Long userId) { 2 return Optional.ofNullable(users.get(userId)); 3}
Jak widzimy, klasę User opakujemy w klasę Optional. Kluczowy tutaj jest fakt, że zmienił się zwracany typ w metodzie - zmieniliśmy API. Użycie może wyglądać następująco:
1findUser(userId) 2 .ifPresent(u -> System.out.println(u.getName()));
Dodaliśmy bardziej funkcyjny charakter do naszego kodu, natomiast to, co najważniejsze to fakt, że teraz fizycznie nie jesteśmy w stanie zapomnieć o obsłużeniu nulla – po prostu kompilator nam na to nie pozwoli.
Użyteczne metody Optionala
Oprócz samego sprawdzania istnienia danego obiektu, do dyspozycji mamy bogate API. Załóżmy, że mamy klasę User z następującymi polami:
1String name; 2 int age; 3List<String> roles
Następnie chcielibyśmy, na podstawie obiekty typu User, wyekstraktować jego nazwę, ale tylko pod warunkiem, że dany użytkownik jest adminem i jest dorosły. Przykładowa metoda spełniająca te wymagania mogłaby wyglądać w ten sposób:
1public String extractAdultAdminName(User user) { 2 return Optional.ofNullable(user) 3 .filter(u -> u.getAge() >= 18) 4 .filter(u -> u.getRoles().contains(“ADMIN”)) 5 .map(u -> u.getName()) 6 .orElse(“”); 7}
Omówmy powyższy przykład po kolei: Najpierw chcemy opakować w Optionala argument metody, gdyż nie wiemy, czy ktoś nie poda nam nulla. Następnie, metoda filter(..) oraz map(..) wykonają się jedynie wtedy, kiedy Optional jest „pełny”. W przeciwnym wypadku zostaną pominięte. Metoda filter(..) , jak wynika z nazwy, filtruje wnętrze Optionala – przejdziemy dalej, jeżeli obiekt siedzący wewnątrz, spełni dany warunek. Metoda map(..) z kolei mapuje obiekt na cokolwiek innego. W powyższym przypadku obiekt typu User zamienimy w Stringa. Na końcu, metoda orElse(..) zwróci nam wewnętrzny obiekt (jeśli tam do tej pory był), lub w przeciwnym wypadku – to, co podamy.
Byłoby prawie dobrze, natomiast zwróćcie uwagę, że przy powyższej implementacji, brak dorosłego admina poskutkuje zwróceniem pustego Stringa. Zatem w miejscu wywołania tej metody, musielibyśmy znowu sprawdzać, czy wynik nie jest pustym Stringiem. Łatwe do przeoczenia. Zatem poprawmy powyższą metodę na ostateczną implementację:
1public Optional<String> extractAdultAdminName(User user) { 2 return Optional.ofNullable(user) 3 .filter(u -> u.getAge() >= 18) 4 .filter(u -> u.getRoles().contains(“ADMIN”)) 5 .map(u -> u.getName()); 6}
Tym sposobem, ktokolwiek wywoła naszą metodę, będzie wiedział, że musi obsłużyć również sytuację, kiedy dorosłego admina brak.
Podsumowanie
Jeżeli wiemy, że nasza metoda może zwrócić nulla, to postarajmy się zamienić zwracany typ na Optionala. Dzięki temu potem, używając gdziekolwiek powyższej metody, możemy być pewni, że nie zapomnimy o obsłużeniu nulla (sprawdzenie na etapie kompilacji, a nie w runtime), a NullPointerException się nie pojawi (a przynajmniej nie z tego powodu).