NullPointerException…

pewnie nie raz udało Ci się otrzymać taki wyjątek w konsoli podczas uruchamiania aplikacji. Wyjątek ten oznacza nic innego jak próba dostania się do pola lub metody obiektu, który tak naprawdę nie istnieje.

Myślisz, że można uporać się ze złowieszczymi gnomami (nullami) unikając zwracania null z metody, która czegoś nie znalazła lub nie może wykonać?

Moim zdaniem tak, dlatego pokażę Ci teraz 3 skuteczne sposoby na uniknięcie NullPointerException Java w obrębie swojej aplikacji.

Wyposażenie przeciwko NullPointerException!

Na początek potrzebujemy stworzyć sobie małe środowisko testowe – będziemy do tego potrzebować prostego modelu Usera:

package pl.maniaq;

public class User {
    private String login;
    private String password;

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    public String getLogin() {
        return login;
    }

    public String getPassword() {
        return password;
    }

    @Override
    public String toString() {
        return "User{" +
                "login='" + login + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

Oraz prostego serwisu, który ma w sobie listę userów:

import java.util.List;

public class UserService {
    private List<User> users;

    public UserService(List<User> users) {
        this.users=users;
    }

}

Klasyczne podejście

Pokażę Ci trzy sposoby na konkretnej metodzie – wyszukiwania usera po loginie – popularny przypadek wyszukiwania elementu, który niestety nie istnieje w bazie.

Klasycznie można zrobić to tak:

public User getUserByLoginReturnsNull(String login) {
    for(User user : users) {
        if (user.getLogin().equals(login)) {
            return user;
        }
    }

    return null;
}

 

Porównania typów prostych oraz obiektów

 

Spójrzmy jeszcze tylko jak zachowuje się taka metoda w akcji:

public class Main {

    public static void main(String[] args) {
      UserService userService = new UserService(Arrays.asList(
              new User("admin", "admin"),
              new User("pablo", "escabo"),
              new User("kasia", "zyt"),
              new User("ufo", "porno")
        ));

      User admin = userService.getUserByLoginReturnsNull("admin");
      User notFoundAdmin = userService.getUserByLoginReturnsNull("notFoundAdmin");

      System.out.println("Admin: " + admin.getLogin());
      System.out.println("NotFoundAdmin: " + notFoundAdmin.getLogin());

    }
}

No i niestety użycie takiej metody w aplikacji wiąże się z niedopuszczalnym dla nas wyjątkiem:

Admin: admin
Exception in thread "main" java.lang.NullPointerException
  at pl.maniaq.Main.main(Main.java:19)

Cel na dziś jest prosty: uniknąć NullPointerException!

1. Rzuć wyjątkiem

rzucanie wyjątkiem java

Pierwszym i za to dosyć prostym rozwiązaniem jest rzucenie wyjątkiem – wbudowanym np. IllegalStateException, jednak lepszym rozwiązaniem będzie stworzenie własnego wyjątku.

public class UserNotFoundException extends Exception {
    public UserNotFoundException(String message) {
        super(message);
    }
}

Mając stworzony taki wyjątek możemy zmienić jedną linijkę w naszej metodzie z:

return null;

na:

throw new UserNotFoundException("User with login: " + login + " not found.");

I cała nasza metoda wygląda teraz tak – dużo lepiej, w końcu nie mamy nulla. 😉

public User getUserByLoginThrowsException(String login) throws UserNotFoundException {
    for(User user : users) {
        if (user.getLogin().equals(login)) {
            return user;
        }
    }

    throw new UserNotFoundException("User with login: " + login + " not found.");
}

Używanie takiej metody wymusza już na nas pamiętanie o możliwości nieznalezienia usera przez konieczność złapania wyjątku – w końcu jest to wyjątek checked, którym musimy się zaopiekować. 😀

package pl.maniaq;

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
      UserService userService = new UserService(Arrays.asList(
              new User("admin", "admin"),
              new User("pablo", "escabo"),
              new User("kasia", "zyt"),
              new User("ufo", "porno")
        ));

 
      try {
        User pablo = userService.getUserByLoginThrowsException("pablo");
        System.out.println("pablo: " + pablo.getLogin());
      } catch (UserNotFoundException e) {
        e.printStackTrace();
      }

      try {
        User notFoundPablo = userService.getUserByLoginThrowsException("notFoundPablo");
        System.out.println("notFoundPablo: " + notFoundPablo.getLogin());
      } catch (UserNotFoundException e) {
        e.printStackTrace();
      }

    }
}

I rezultat działania takiego programu wygląda już tak:

pablo: pablo
pl.maniaq.UserNotFoundException: User with login: notFoundPablo not found.
  at pl.maniaq.UserService.getUserByLoginThrowsException(UserService.java:30)
  at pl.maniaq.Main.main(Main.java:30)

Jak widać pozbyliśmy się już wyjątku unchecked: NullPointerException – nasz kod jest pozbawiony żadnych nieoczekiwanych rozwiązań – w końcu aplikacja może się wykrzaczyć otrzymując nulla.

2. Default Object

Zwracanie default objectu Java

Jednym z rozwiązań może też być zwrócenie tak zwanego default objectu, czyli obiektu o jakiś domyślnych wartościach. Stwórzmy sobie taki obiekt w klasie User jako stałą statyczną, a następnie pokażę Ci jak łatwo z niego skorzystać.

public final static User DEFAULT_USER = new User("annonymous", "password");

I bez problemu możemy już stworzyć taką metodę:

public User getUserByLoginReturnsDefaultObject (String login) {
    for(User user : users) {
        if (user.getLogin().equals(login)) {
            return user;
        }
    }

    return User.DEFAULT_USER;
}

Choć nie jest to wymarzone rozwiązanie to jednak i tak nas potrafi uchronić przez złowieszczym nullem. 😉

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
      UserService userService = new UserService(Arrays.asList(
              new User("admin", "admin"),
              new User("pablo", "escabo"),
              new User("kasia", "zyt"),
              new User("ufo", "porno")
        ));

    User secondPablo = userService.getUserByLoginReturnsDefaultObject("pablo-2");
    System.out.println("Second pablo: " + secondPablo.getPassword());

    }
}

I otrzymujemy taki rezultat:

Second pablo: password

Nie mamy NullPointerException – cieszymy się i idziemy dalej. 😉

3. Optional<T>

Użycie Optional Java

Najlepszym – przynajmniej według mnie i wielu innych programistów – jest używanie klasy osłonowej Optional, która weszła w Javie 8.

W dokumentacji Javy przeczytamy coś takiego:

A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value.

Jak zwał tak zwał – czy to klasa osłonowa czy kontener to nieważne – ważne, że chroni nas przed nullami.

Klasa Optional<T> korzysta z typów generycznych – dokładnie chodzi o te nawiasy i literę T – czyli opakowuje typ T w siebie – damy mu User to opakuje nam Usera.

Spójrzmy od razu na przykład i już wszystko tłumaczę:

public Optional<User> getUserByLoginReturnsOptional(String login) {
    for(User user : users) {
        if (user.getLogin().equals(login)) {
            return Optional.of(user);
        }
    }

    return Optional.empty();
}

Korzystamy z dwóch metod statycznych – of oraz empty. Metoda of pozwala na opakowanie naszego Usera właśnie w kontener – za to metoda empty() zwraca pusty obiekt User – zamiast nieszczęsnego nulla.

No dobra zastanowisz się – a jak wygląda używanie takiej metody?

Już pokazuję i przy okazji powiem – bardzo prosto.

Optional<User> foundKasia = userService.getUserByLoginReturnsOptional("kasia");
if (foundKasia.isPresent()) {
  System.out.println("Found kasia: " + foundKasia.get());
}

Klasa Optional daje nam m.in dwie metody: isPresent – jeśli kontener zawiera jakiś obiekt, get – zwraca obiekt – uwaga może rzucić nam wyjątkiem tak jak my to robiliśmy w punkcie 1.

Jednak połączenie metod isPresent i get jest bez sensu – w końcu wracamy do genezy, czyli do zwykłego porównaniania czy obiekt jest nullem, tylko trochę w innej postaci. 😉 Optional posiada lepsze metody do obsługi nulli.

If a value is present in this Optional, returns the value, otherwise throws NoSuchElementException.

Możemy również zrobić to inaczej – zwrócić konkretny obiekt, jeśli kontener jest pusty – używając metody orElse:

User notFoundKasia = userService.getUserByLoginReturnsOptional("notFoundKasia").orElse(User.DEFAULT_USER);
System.out.println("notFoundKasia: " + notFoundKasia.getPassword());

Gdy nie znajdzie usera o logicznie notFoundKasia – a nie znajdzie 😉 – zwróci nam defaultowy obiekt Usera.

Optional dostarcza jeszcze wiele innych opcji – m.in rzucanie swojego wyjątku, gdy obiekt nie znajduje się w konterze.

try {
  User kasia = userService.getUserByLoginReturnsOptional("kasia")
      .orElseThrow(() -> new UserNotFoundException("User with login kasia not found."));
  System.out.println("kasia: " + kasia.getPassword());
} catch (UserNotFoundException e) {
  e.printStackTrace();
}

Uwaga: Zapis() -> new UserNotFoundException(„User with login kasia not found.”) oznacza wykorzystanie wyrażeń lambda, które weszły również w Javie 8.

Nie uważasz, że Optionale są bardzo proste w użyciu i dzięki nim unikniemy nielubianego NullPointerException? 😉

4. BONUS – Java streams

Skoro dotarłeś już tak daleko i trochę ruszyliśmy Javy 8 to warto również pokazać implementację metody getUserByLogin korzystając ze streamów.

Nie jest skomplikowana – wygląda tak:

public Optional<User> getUserByLoginReturnsOptionalUsingStreams(String login) {
    return users.stream()
            .filter(user -> user.getLogin().equals(login))
            .findFirst();
}

Rozwiązanie czytelniejsze niż z wykorzystaniem pętli – choć pod spodem implementacja jest podobna lub nawet taka sama. 😉

 

Kolekcje w javie

 

stream – oznacza stworzenie strumienia na kolekcji – u nas jest to lista,

filter – oznacza przefiltrowanie kolekcji po danym kryterium – u nas jest to równość loginów,

findFirst – wyciągnięcie pierwszego obiektu z przefiltrowanej kolekcji.

Uwaga: Zauważ, że Java Streams po swoich operacjach również zwraca kontener Optional!

A użycie takiej metody wygląda równie podobnie jak metoda korzystająca z pętli:

User ufo = userService.getUserByLoginReturnsOptionalUsingStreams("ufo").orElse(User.DEFAULT_USER);
User notFoundUfo = userService.getUserByLoginReturnsOptionalUsingStreams("notFoundUfo").orElse(User.DEFAULT_USER);

System.out.println("ufo: " + ufo.getLogin());
System.out.println("notFoundUfo: " + notFoundUfo.getLogin());

Podsumowanie

Przedstawiłem Ci powyżej 3 proste sposoby na unikanie NullPointerException w obrębie swojej aplikacji. Zapamiętaj choć jedno z powyższych rozwiązań, a Twój kod będzie mniej podatny na błędy. Nie ma co się oszukiwać, na pewno namawiam Cię do używania kontenera Optional – no chyba, że niestety nie możesz w projekcie korzystać co najmniej 8 wersji Javy.

Bonusowo pokazałem Ci Java Streams, na pewno warto nauczyć się tzw. streamów, aby twój kod był krótszy (czyt. czytelniejszy, czystszy).

Kod z całego artykułu możesz podejrzeć na moim repozytorium tzn. dokładnie tutaj.

A na koniec Ci życzę jak najmniej NullPointerExceptionów!