Czy na pewno tu nie będzie nulla?

java.lang.NullPointerException. Każdy go zna, każdy uwielbia, a debugując enterprisową aplikację (napisaną oczywiście przez kogoś innego, my byśmy takiego błędu nie popełnili!), natrafiwszy na taki zapis w logach można dostać białej gorączki. I niby każdy wie, że powinno się stosować null-checki w newralgicznych miejscach. Tak samo jak każdy wie, że jakby co to są Opszonale. A i tak w tym jednym jedynym miejscu ani null-checka ani typu Optional nie będzie, bo przecież tam i tak nie będzie nulla. Zapraszam do lektury.

Początkowo ten wpis dotyczyć miał tylko typów opcjonalnych, ale w trakcie pisania doszedłem do wniosku, że Optional nie jest lekarstwem na wszystko. Mając bowiem odpowiednio rozwiniętą wyobraźnię, można przez wiele lat pisać działający bardzo poprawnie kod i nie wpisać w IntelliJ (Eclipse, Netbeans, JDevelopera czy czegokolwiek używasz) wyrazu Optional. Nie oznacza to oczywiście, że tak będzie najlepiej, ale odpowiednie stawianie null-checków to także jest sztuka.

Gdzie teoretycznie powinien pojawić się null-check? W teorii wszędzie tam, gdzie próba wykonania jakiejść instrukcji może skutkować wyrzuceniem wyjątku NullPointerException. Niestety, takie działanie zmuszałoby nas do opakowywanie ifami prawie każdej instrukcji, w Javie przecież większość rzeczy, jakie robimy to operacje na obiektach. W praktyce zatem staramy się przewidzieć gdzie nulle mogą wystąpić a gdzie nie. Dla przykładu – pobierając encję z bazy danych możemy mieć przekonanie graniczące z pewnością, że wszelkie pola walidowane przy zapisie jako nienullowe – będą miały wartość. Przy pozostałych – warto pokusić się o null-checki.

Jak wygląda typowy null-check? Znajdujemy fragment kodu wrażliwy na NullPointerException i opakowujemy go w warunek sprawdzający, czy aby na pewno obiekt, na którym chcemy operować ma wartość.

Dla przykładu wywołanie tego kodu zaskutkuje nam NPE:

public class Application {

    public static void main(String[] args){
        User user = null;

        if (user.getName().contains("user")) {
            System.out.println("This is user");
        }
    }

}

I mamy w konsoli:

Exception in thread "main" java.lang.NullPointerException
  at it.gniado.application.Application.main(Application.java:10)

Process finished with exit code 1

Jak temu zaradzić? Najprostszy sposób to opakowanie wrażliwego miejsca (linia 10 – user.getName()) w najprostszy warunek porównujący usera do nulla, co zmieni wrażliwy warunek w taką postać:

if (user != null)
    if (user.getName().contains("user")) {
        System.out.println("This is user");
    }
}

Dzięki temu wywołanie user.getName() jest bezpieczne – jeśli user jest nullem, to nie dojdzie do wywołania problematycznego kodu, natomiast jeśli nie jest – kod powinien się wywołać bezproblemowo. Czy aby na pewno? Ustawmy nowy obiekt typu User jako wartość zmiennej user i spróbujmy uruchomić aplikację.

public class Application {

    public static void main(String[] args){
        User user = new User();

        if (user != null) {
            if (user.getName().contains("user")) {
                System.out.println("This is user");
            }
        }
    }

}
Exception in thread "main" java.lang.NullPointerException
  at it.gniado.application.Application.main(Application.java:11)

Process finished with exit code 1

Co tym razem? No właśnie. Zaraz po pobraniu atrybutu name, próbujemy… Wykonać metodę contains na obiekcie name typu String. A jeśli String jest nullem…? W tym wypadku należy rozszerzyć null-check o kolejny obiekt:

if (user != null && user.getName() != null) {
    if (user.getName().contains("user")) {
        System.out.println("This is user");
    }
}

Jak widać teraz najpierw sprawdzamy, czy user jest nullem, a jeśli nie jest – sprawdzamy czy null zwróci wywołanie metody getName(). Nie jest to błąd, ba – jest to wręcz zapobieżenie błędowi, ponieważ teraz po uruchomieniu wyjątek nie zostanie wyrzucony.

Trochę bardziej elegancki sposób

Jest jednak trochę bardziej eleganckie podejście do przedstawionego problemu. Podejście, które zostało zaproponowane nam wraz z premierą Javy 8, czyli… ładnych parę lat temu. W enterprise’owym świecie jednak wszystko kręci się duuużo wolniej (dość powiedzieć że wiele korporacji pracuje nadal na… Javie 1.6!) i w wielu miejscach jest duże pole do popisu jeśli chodzi o refaktoryzację. Jednym z takich eleganckich podejść jest zastosowanie typu Optional.

Problem z typami opcjonalnymi jest taki, że programiści często nie wiedzą jak z niego skorzystać. Wykorzystują dwie podstawowe metody typu Optional i tak naprawdę… robią zwykłe null-checki obudowując je w fancy sposób. Bo jak inaczej nazwać taką konstrukcję, wykorzystującą podstawowe metody Optional – isPresent() oraz get():

public class User {

    private Optional<String> name;

    public Optional<String> getName(){
        return name;
    }

    public void setName(String name){
        this.name = Optional.ofNullable(name);
    }
}




public class Application {

    public static void main(String[] args){
        Optional<User> user = Optional.ofNullable(new User());

        if (user.isPresent() && user.get().getName().isPresent()) {
            if (user.get().getName().get().contains("user")) {
                System.out.println("This is user");
            }
        }
    }

}

Jak można zobaczyć w klasie Application – nie zmieniło się nic, poza tym, że zamiast “zwykłego” != null mamy “fajne i nowoczesne” .isPresent(). Dodatkowo konstrukcja w wewnętrznym ifie jest groteskowa – user.get().getName().get().contains… dafuq?

Takie konstrukcje mają miejsce z jednego powodu – programiści chcą korzystać z nowych technologii i nowych rozwiązań, ale nie mają czasu (lub nie chcą) się ich uczyć. Wykorzystują więc to, co już znają, ale w sposób trochę bardziej skomplikowany, karmiąc się iluzją o wartościowości ich kodu.

Optional przychodzi nam z pomocą w takich sytuacjach, na początek uprośćmy nasz kod – niech napisze “this is user” gdy chociaż pierwszy warunek jest spełniony:

public class Application {

    public static void main(String[] args){
        User user = new User();

        if (user != null){
            System.out.println("This is user");
        }

    }

}

Taką konstrukcję łatwo poprawić wykorzystując nullable, a więc tworząc coś następującego:

public class Application {

    public static void main(String[] args){
        User user = new User();

        Optional.ofNullable(user).ifPresent(System.out::println);

    }

}

Jest to o tyle nietrafne, że w tym wypadku nie wydrukujemy “This is user” a jedynie wywołamy metodę toString() z klasy User. Aby program odzwierciedlał wcześniejsze założenie należy skorzystać z wyrażenia lambda mapującego obiekt user:

public class Application {

    public static void main(String[] args){
        User user = new User();

        Optional.ofNullable(user).ifPresent(u -> {
            System.out.println("This is user");
        });

    }

}

Obecnie kod jest identyczny jak ten przedstawiony dwa listingi temu – wykorzystujący ify i klasyczne null-checki. Mając już możliwość odnieść się do obiektu user, możemy rozszerzyć konstrukcję Optional o kolejny nowoczesny null-check:

public class Application {

    public static void main(String[] args){
        User user = new User();

        Optional.ofNullable(user).ifPresent(u -> {
            Optional.ofNullable(u.getName()).ifPresent(v -> {
                System.out.println("This is user");
            });
        });

    }

}

Mamy już prawie to, co wyżej. Nasza aplikacja w tym momencie sprawdzi, czy obiekt user ma wartość, i jeśli tak – dokona ponownego sprawdzenia na polu name tegoż obiektu (wyrażenie u.getName() – u jest naszym userem!). Jeśli name także jest nienullowe – wypisany zostanie na standardowe wyjście napis “This is user”. Jest to nadal nie do końca taka forma, jak na samym początku (sprawdzano jeszcze, czy user.getName() zawiera ciąg “user”), ale wiemy już jak się dostać do wnętrza parametru name (wyrażenie v):

public class Application {

    public static void main(String[] args){
        User user = new User();

        Optional.ofNullable(user).ifPresent(u -> {
            Optional.ofNullable(u.getName()).ifPresent(v -> {
                if (v.contains("user")) {
                    System.out.println("This is user");
                }
            });
        });

    }

}

Ten kod spełnia w całości podstawowe założenia, i teraz należy samemu sobie odpowiedzieć na pytanie który jest czytelniejszy, czy ten powyżej, czy ten poniżej:

public class Application {

    public static void main(String[] args){
        User user = new User();

        if (user != null){
            if (user.getName() != null){
                if (user.getName().contains("user")) {
                    System.out.println("This is user");
                }
            }
        }

    }

}

Pozostałe informacje o Optional

Tworzenie Optionali można wykonać za pomocą jednej z czterech metod:

  • .empty() – tworzy pusty Optional z wartością null w środku
  • .of(value) – tworzy obiekt Optional z wartością przekazaną jako parametr. Przekazanie nulla zaowocuje NullPointerException
  • .ofNullable(value) – jak wyżej, jednak przekazanie nulla nie spowoduje wyrzucenia wyjątku
  • klasyczny konstruktor argumentowy wyrzucający wyjątek w przypadku przesłania nulla

Problemem związanym z Optionalami byłoby także nadużywanie go. Nie powinno się wykorzystywać typów opcjonalnych jako pól w POJO. O wiele lepiej wykorzystać Optionale w getterach lub setterach do tych pól. Zamiana getterów na takie, które zwracają Optionale powoduje, że pobierając pole mamy od razu dostęp do typu opcjonalnego, wraz ze wszystkimi jego zaletami. Zmodyfikujmy getter w klasie User:

public class User {

    private String name;

    public Optional getName(){
        return Optional.ofNullable(name);
    }

    public void setName(String name){
        this.name = name;
    }
}

Dzięki temu klasa aplikacji zmieniłaby się na:

public class Application {

    public static void main(String[] args){
        User user = new User();

        Optional.ofNullable(user).ifPresent(u -> {
            u.getName().ifPresent(n -> {
                if (((String) n).contains("user")) {
                    System.out.println("This is user");
                }
            });
        });

    }

}

Wykorzystanie Optionali może przynieść dużo przejrzystości czy nawet czystości do naszego kodu. Będzie on oczywiście bardziej skomplikowany, ale jest to poziom skomplikowania typu skorzystania z pętli for zamiast z pętli while. Typ Optional jest po prostu kolejną nowością w języku, która może ułatwić nam życie, dlatego warto się jej nauczyć.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *