Menu

NullPointerException w Javie: przyczyny i jak go naprawić

Co naprawdę oznacza NullPointerException w Javie, jakie są typowe sposoby na jego wywołanie, jak czytać komunikat i jakie wzorce mu zapobiegają.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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. Po Collections.emptyList() i "" można bezpiecznie iterować i wywoływać na nich metody.
  • Używaj getOrDefault na mapach, żeby nieudane wyszukiwanie dawało prawdziwą wartość zamiast null.
  • 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ