Najczęstszy błąd w Javie
NullPointerException (wszyscy mówią na niego NPE) pojawia się, gdy próbujesz użyć referencji, która nie wskazuje na nic, czyli na null, tak jakby wskazywała na prawdziwy obiekt. Zmienne typu obiektowego w Javie zawierają albo obiekt, albo null, wartość "tu nie ma obiektu". Gdy tylko poprosisz null, żeby coś zrobił, czyli wywołasz na nim metodę, odczytasz jego pole albo sięgniesz do niego indeksem, nie ma na czym działać i JVM rzuca wyjątek.
W odróżnieniu od try-catch, po który sięgasz na poprzedniej stronie, żeby obsłużyć oczekiwane błędy, NPE to prawie zawsze zwykły błąd w kodzie. Celem tej strony nie jest ich łapanie, tylko zrozumienie, dlaczego się pojawiają, i pisanie kodu, który ich nie powoduje.
Nie ma obiektu String, na którym mogłoby działać length(), więc program zatrzymuje się z NullPointerException.
Czytanie komunikatu
Od Javy 14 komunikat mówi dokładnie, co było null; nazywa się to Helpful NullPointerException. Przeczytaj go, zanim cokolwiek zmienisz:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Main.main(Main.java:4)
Liczą się dwie części. Cannot invoke "String.length()" to operacja, która się nie udała, a because "name" is null wskazuje winowajcę. Linia at Main.main(Main.java:4) to stack trace wskazujący dokładną linię. Poprawka to więc nie "owiń linię 4 w try", tylko "ustal, dlaczego name jest null w linii 4". Prawie zawsze błąd jest gdzieś wcześniej, tam gdzie wartość powinna zostać przypisana, a nie została.
Sposoby na wywołanie NPE
NPE bierze się z kilku operacji, a wszystkie to warianty "dotykania null":
Wyszukiwanie w mapie to klasyczne źródło: get zwraca null, gdy klucza nie ma, a NPE często wychodzi kilka linii później, gdy w końcu używasz tej wartości. Rozpakowywanie (unboxing) jest podstępne: przypisanie Integer równego null do int rzuca wyjątek, bo nie ma liczby do skopiowania.
Ochrona przez sprawdzenie null
Najprostsza obrona to if, który przed użyciem potwierdza, że referencja nie jest null:
Gdy porównujesz zmienną ze stałym String, stałą wpisz jako pierwszą: "yes".equals(answer) zamiast answer.equals("yes"). Jeśli answer to null, pierwsza forma spokojnie zwraca false, a druga rzuca wyjątek.
Szybka porażka z Objects.requireNonNull
Rozrzucanie sprawdzeń null wszędzie robi się uciążliwe. Gdy wartość nigdy nie powinna być null, np. argument konstruktora, sprawdź ją na granicy przez Objects.requireNonNull. Rzuca wyjątek od razu, z jasnym komunikatem, w miejscu, w którym pojawia się zła wartość, a nie później głęboko w kodzie:
Ten nawyk "fail fast" zamienia niejasny NPE 200 linii dalej w precyzyjną skargę u źródła. Łapiemy tu wyjątek tylko po to, by pokazać komunikat; w prawdziwym kodzie lepiej pozwolić mu wyjść na wierzch, żeby błąd został naprawiony.
Unikanie null od samego początku
Najlepszy NPE to taki, który nie może się zdarzyć, bo nie ma null, na który można trafić. Kilka nawyków bardzo pomaga:
- Zwracaj pustą kolekcję lub pusty napis, nigdy
null. PoCollections.emptyList()i""można bezpiecznie iterować i wywoływać na nich metody. - Używaj
getOrDefaultna mapach, żeby nieudane wyszukiwanie dawało prawdziwą wartość zamiastnull. - Inicjalizuj pola przy deklaracji, zamiast zostawiać je jako
null"na później".
Gdy wartość jest naprawdę opcjonalna, np. wyszukiwanie, które może zgodnie z zasadami niczego nie znaleźć, Java oferuje Optional: kontener, który zmusza wywołującego do obsłużenia przypadku "brak wartości" zamiast po cichu oddawać null. To powiązane pojęcie warto przeczytać jako następne, jeśli chcesz całkowicie usunąć takie luki ze swoich API.
Podsumowanie
NullPointerException to sygnał od Javy, że referencja zawierająca null została użyta tak, jakby zawierała obiekt. Rozwiązaniem rzadko jest łapanie wyjątku. Przeczytaj pomocny komunikat, cofnij się do miejsca, w którym wartość powinna zostać ustawiona, i albo zagwarantuj, że nie jest null, albo zabezpiecz miejsce jej użycia. Korzystaj z Objects.requireNonNull, żeby szybko zgłaszać błąd na granicach, wybieraj puste wartości i getOrDefault zamiast null, a po Optional sięgaj, gdy brak wartości jest prawdziwym, oczekiwanym wynikiem. Opanuj ten sposób myślenia, a najczęstszy błąd w Javie stanie się jednym z najrzadszych w twoim kodzie.
Najczęściej zadawane pytania
Co powoduje NullPointerException w Javie?
Pojawia się, gdy używasz referencji wskazującej na null tak, jakby wskazywała na prawdziwy obiekt: np. wywołujesz metodę (name.length()), czytasz pole, sięgasz do elementu tablicy albo rozpakowujesz Integer równy null. Zmienna nie zawiera obiektu, więc nie ma na czym działać i JVM rzuca NullPointerException.
Jak naprawić NullPointerException w Javie?
Przeczytaj komunikat: od Javy 14 mówi on dokładnie, co było null (np. "Cannot invoke "String.length()" because "name" is null"). Potem ustal, dlaczego ta zmienna jest null: brak inicjalizacji, metoda, która zwróciła null, albo nieudane wyszukiwanie w mapie. Popraw źródło, żeby wartość nigdy nie była null, albo zabezpiecz jej użycie sprawdzeniem null, Objects.requireNonNull lub Optional.
Lepiej sprawdzać null czy łapać NullPointerException?
Sprawdzaj null. NullPointerException sygnalizuje błąd w logice, a nie oczekiwaną sytuację, więc należy mu zapobiegać, a nie go łapać. Złapanie go ukrywa, gdzie leży prawdziwy problem. Zostaw try/catch na naprawdę wyjątkowe sytuacje, np. błędy wejścia/wyjścia.