Po co jawnie zgłaszać wyjątek NullPointerException, zamiast pozwalać, aby stało się to naturalnie?

182

Podczas czytania kodu źródłowego JDK często zdarza się, że autor sprawdza parametry, jeśli są one puste, a następnie ręcznie rzuca nowy wyjątek NullPointerException (). Dlaczego oni to robią? Myślę, że nie ma takiej potrzeby, ponieważ wywoła nową NullPointerException (), gdy wywoła dowolną metodę. (Oto przykładowy kod źródłowy HashMap :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}
LiJiaming
źródło
32
kluczowym punktem kodowania jest zamiar
Scary Wombat
19
To bardzo dobre pytanie do pierwszego pytania! Wprowadziłem kilka drobnych zmian; Mam nadzieję, że nie masz nic przeciwko. Usunąłem również podziękowania i notatkę o tym, że jest to twoje pierwsze pytanie, ponieważ zwykle takie rzeczy nie są częścią SO.
David Conrad
11
Jestem C # konwencja ma być podnoszona ArgumentNullExceptionw takich przypadkach (a nie NullReferenceException) - w rzeczywistości jest to naprawdę dobre pytanie, dlaczego podnosisz NullPointerExceptionjawnie tutaj (zamiast innego).
EJoshuaS - Przywróć Monikę
21
@EJoshuaS To stara debata na temat tego, czy rzucić IllegalArgumentExceptionlub NullPointerExceptiono zerowy argument. Konwencja JDK jest tym drugim.
shmosel
33
PRAWDZIWY problem polega na tym, że zgłaszają błąd i wyrzucają wszystkie informacje, które doprowadziły do ​​tego błędu . Wygląda na to, że jest to prawdziwy kod źródłowy. Nawet zwykły krwawy ciąg znaków. Smutny.
Martin Ba

Odpowiedzi:

254

Przychodzi mi na myśl szereg przyczyn, z których kilka jest ściśle powiązanych:

Szybka awaria: jeśli zawiedzie, najlepiej raczej zawieść wcześniej niż później. Umożliwia to wychwycenie problemów bliżej ich źródła, co ułatwia ich identyfikację i odzyskanie. Pozwala także uniknąć marnowania cykli procesora na kod, który z pewnością zawiedzie.

Zamiar: Zgłoszenie wyjątku wyraźnie mówi opiekunom, że błąd występuje tam celowo, a autor był świadomy konsekwencji.

Spójność: jeśli pozwolono, aby błąd wystąpił naturalnie, może nie wystąpić w każdym scenariuszu. Jeśli na przykład nie zostanie znalezione mapowanie, remappingFunctionnigdy nie zostanie użyte, a wyjątek nie zostanie zgłoszony. Wcześniejsza weryfikacja danych wejściowych pozwala na bardziej deterministyczne zachowanie i bardziej przejrzystą dokumentację .

Stabilność: Kod ewoluuje z czasem. Kod napotkany wyjątek w naturalny sposób może, po pewnym przeredagowaniu, przestać to robić lub zrobić to w różnych okolicznościach. Rzucanie go jawnie zmniejsza prawdopodobieństwo przypadkowej zmiany zachowania.

Shmosel
źródło
14
Ponadto: w ten sposób lokalizacja, z której zgłaszany jest wyjątek, jest powiązana z dokładnie jedną sprawdzaną zmienną. Bez tego wyjątek może wynikać z faktu, że jedna z wielu zmiennych ma wartość NULL.
Jens Schauder,
45
Kolejny: jeśli zaczekasz, aż NPE wydarzy się naturalnie, jakiś kod pośredni mógł już zmienić stan twojego programu z powodu efektów ubocznych.
Thomas
6
Chociaż ten fragment tego nie robi, możesz użyć new NullPointerException(message)konstruktora, aby wyjaśnić, co jest null. Dobry dla osób, które nie mają dostępu do Twojego kodu źródłowego. Zrobili to nawet jednowierszowe w JDK 8 Objects.requireNonNull(object, message)metodą użytkową.
Robin
3
AWARIA powinna znajdować się w pobliżu AWARII. „Fail Fast” to więcej niż ogólna zasada. Kiedy nie chcesz tego zachowania? Każde inne zachowanie oznacza, że ​​ukrywasz błędy. Istnieją „WADY” i „AWARIE”. AWARIA występuje, gdy ten program przetwarza wskaźnik NULL i ulega awarii. Ale w tym wierszu kodu nie ma błędu. NULL pochodzi skądś - argument metody. Kto przeszedł ten argument? Z jakiegoś wiersza kodu odwołującego się do zmiennej lokalnej. Skąd to ... Widzisz? To jest do bani. Czyją odpowiedzialnością powinno być obserwowanie gromadzenia złej wartości? Twój program powinien się wtedy zawiesić.
Noah Spurrier
4
@Thomas Dobra uwaga. Shmosel: Punkt Thomasa może wynikać z punktu szybkiego, ale jest w pewnym sensie zakopany. Jest to na tyle ważna koncepcja, że ​​ma swoją własną nazwę: atomowość awarii . Patrz Bloch, Effective Java , pozycja 46. Ma silniejszą semantykę niż fail-fast. Sugerowałbym powołanie się na to w osobnym punkcie. Doskonała odpowiedź ogólnie, BTW. +1
Znaki Stuarta
40

Ma to na celu zapewnienie jasności, spójności i zapobieganie dodatkowej, niepotrzebnej pracy.

Zastanów się, co by się stało, gdyby na szczycie metody nie było klauzuli ochronnej. Zawsze zadzwoni, hash(key)a getNode(hash, key)nawet jeśli nullzostanie wydany remappingFunctionprzed wyrzuceniem NPE.

Co gorsza, jeśli if warunek jest spełniony false, bierzemy elsegałąź, która w ogóle nie używa remappingFunction, co oznacza, że ​​metoda nie zawsze wyrzuca NPE, gdynull przejściu a; czy tak, zależy od stanu mapy.

Oba scenariusze są złe. Jeśli nullnie jest prawidłową wartością dlaremappingFunction metoda powinna konsekwentnie zgłaszać wyjątek bez względu na wewnętrzny stan obiektu w momencie wywołania i powinna to robić bez wykonywania zbędnej pracy, która jest bezcelowa, biorąc pod uwagę, że po prostu rzuci. Wreszcie, dobrą zasadą czystego, przejrzystego kodu jest umieszczenie straży bezpośrednio, aby każdy, kto przejrzy kod źródłowy, mógł łatwo zobaczyć, że to zrobi.

Nawet jeśli wyjątek byłby obecnie zgłaszany przez każdą gałąź kodu, możliwe jest, że przyszła wersja tego kodu to zmieni. Przeprowadzenie kontroli na początku zapewnia, że ​​na pewno zostanie wykonana.

David Conrad
źródło
25

Oprócz powodów wymienionych przez doskonałą odpowiedź @ shmosel ...

Wydajność: Mogą występować / były korzyści z wydajności (w niektórych maszynach JVM) wynikające z jawnego rzucania NPE, a nie pozwalania JVM na to.

Zależy to od strategii, jaką interpreter Java i kompilator JIT stosują do wykrywania dereferencji wskaźników zerowych. Jedną strategią jest nie testowanie na wartość NULL, ale zamiast tego pułapkowanie SIGSEGV, które ma miejsce, gdy instrukcja próbuje uzyskać dostęp do adresu 0. Jest to najszybsze podejście w przypadku, gdy odwołanie jest zawsze ważne, ale jest drogie w przypadku NPE.

Wyraźny test nullw kodzie pozwoliłby uniknąć trafienia wydajności SIGSEGV w scenariuszu, w którym NPE były częste.

(Wątpię, czy byłaby to opłacalna mikrooptymalizacja w nowoczesnej maszynie JVM, ale mogło tak być w przeszłości).


Zgodność: prawdopodobnym powodem braku komunikatu w wyjątku jest zgodność z NPE generowanymi przez samą JVM. W zgodnej implementacji Java komunikat NPE wygenerowany przez JVM ma nullkomunikat. (Android Java jest inny.)

Stephen C.
źródło
20

Oprócz tego, co zauważyli inni ludzie, warto tutaj zwrócić uwagę na rolę konwencji. Na przykład w języku C # masz również tę samą konwencję jawnego zgłaszania wyjątku w takich przypadkach, ale jest to konkretna ArgumentNullException, która jest nieco bardziej szczegółowa. (Konwencja C # jest taka, że NullReferenceException zawsze reprezentuje jakiś rodzaj błędu - po prostu nie powinno się to nigdy zdarzać w kodzie produkcyjnym; oczywiście ArgumentNullException, również, ale może to być błąd bardziej podobny do „nie zrozumieć, jak prawidłowo korzystać z biblioteki (rodzaj błędu).

Zasadniczo więc w języku C # NullReferenceExceptionoznacza, że ​​twój program faktycznie próbował go użyć, podczas ArgumentNullExceptiongdy oznacza to, że rozpoznał, że wartość była niepoprawna i nawet nie zadał sobie trudu, aby go użyć. Implikacje mogą być różne (w zależności od okoliczności), ponieważ ArgumentNullExceptionoznacza, że ​​dana metoda nie miała jeszcze skutków ubocznych (ponieważ nie spełniła warunków wstępnych metody).

Nawiasem mówiąc, jeśli podbijasz coś w stylu ArgumentNullExceptionlub IllegalArgumentException, jest to po prostu część sprawdzania: chcesz innego wyjątku, niż normalnie.

Tak czy inaczej, jawne zgłoszenie wyjątku wzmacnia dobrą praktykę wyrażania się jasno na temat warunków wstępnych i oczekiwanych argumentów metody, co ułatwia czytanie, używanie i utrzymywanie kodu. Jeśli nie nullzaznaczyłeś jawnie , nie wiem, czy to dlatego, że myślałeś, że nikt nigdy nie przejdzie nullargumentu, liczysz się z tym, że i tak rzuci wyjątek, albo po prostu zapomniałeś go sprawdzić.

EJoshuaS - Przywróć Monikę
źródło
4
+1 za środkowy akapit. Argumentowałbym, że dany kod powinien „zgłosić nowy wyjątek IllegalArgumentException („ funkcja remapowania nie może mieć wartości null ”);” W ten sposób od razu widać, co było nie tak. Przedstawiony NPE jest nieco niejednoznaczny.
Chris Parker,
1
@ChrisParker Byłem kiedyś tego samego zdania, ale okazuje się, że wyjątek NullPointerException ma oznaczać argument zerowy przekazany do metody oczekującej argumentu różnego od wartości, oprócz tego, że jest odpowiedzią środowiska wykonawczego na próbę wyzerowania wartości zerowej. Z javadoc: „Aplikacje powinny zgłaszać instancje tej klasy w celu wskazania innych nielegalnych zastosowań nullobiektu.” Nie szaleję za tym, ale wydaje się, że to zamierzony projekt.
VGR
1
Zgadzam się, @ChrisParker - Myślę, że ten wyjątek jest bardziej szczegółowy (ponieważ kod nigdy nawet nie próbował zrobić nic z wartością, natychmiast rozpoznał, że nie powinien go używać). W tym przypadku podoba mi się konwencja C #. Konwencja C # jest taka, że NullReferenceException(odpowiednik NullPointerException) oznacza, że ​​twój kod faktycznie próbował go użyć (co zawsze jest błędem - nigdy nie powinno się zdarzyć w kodzie produkcyjnym), a „wiem, że argument jest błędny, więc nawet nie spróbuj go użyć ”. Jest też ArgumentException(co oznacza, że ​​argument był błędny z innego powodu).
EJoshuaS - Przywróć Monikę
2
Powiem tyle, ZAWSZE zgłaszam wyjątek IllegalArgumentException zgodnie z opisem. Zawsze czuję się komfortowo, lekceważąc konwencję, kiedy czuję, że konwencja jest głupia.
Chris Parker,
1
@PieterGeerkens - tak, ponieważ linia 35 NullPointerException jest znacznie lepsza niż linia 35 IllegalArgumentException („Funkcja nie może mieć wartości null”). Poważnie?
Chris Parker
12

Jest tak, że otrzymasz wyjątek, gdy tylko popełnisz błąd, a nie później, gdy będziesz korzystać z mapy i nie zrozumiesz, dlaczego tak się stało.

Markiz Lorne
źródło
9

Zmienia pozornie błędny warunek błędu w wyraźne naruszenie umowy: funkcja ma pewne warunki wstępne do poprawnego działania, więc sprawdza je wcześniej, wymuszając ich spełnienie.

Skutkuje to tym, że nie będziesz musiał debugować, computeIfPresent()gdy wyjmiesz z niego wyjątek. Gdy zobaczysz, że wyjątek pochodzi z kontroli warunków wstępnych, wiesz, że wywołałeś funkcję z niedozwolonym argumentem. Gdyby nie było czeku, należy wykluczyć możliwość, że istnieje w nim jakiś błąd computeIfPresent(), który prowadzi do zgłoszenia wyjątku.

Oczywiście rzucenie NullPointerExceptionleku generycznego jest naprawdę złym wyborem, ponieważ nie oznacza samo w sobie naruszenia umowy. IllegalArgumentExceptionbyłby lepszym wyborem.


Sidenote:
Nie wiem, czy Java na to pozwala (wątpię w to), ale programiści C / C ++ używają assert()w tym przypadku opcji, która jest znacznie lepsza do debugowania: Mówi programowi, aby zawiesił się natychmiast i tak mocno, jak to możliwe, jeśli pod warunkiem warunek oceniany na false. Więc jeśli uciekłeś

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

pod debuggerem i coś przechodziło NULLdo któregokolwiek argumentu, program zatrzymywałby się tuż przy linii NULLi wskazywał, który argument był , a ty mógłbyś w dowolnym momencie zbadać wszystkie lokalne zmienne całego stosu wywołań.

cmaster - przywróć monikę
źródło
1
assert something != null;ale to wymaga -assertionsflagi podczas uruchamiania aplikacji. Jeśli -assertionsflagi nie ma, słowo kluczowe assert nie zgłosi wyjątku AssertionException
Zoe
Zgadzam się, dlatego wolę tutaj konwencję C # - odwołanie zerowe, niepoprawny argument i argument zerowy ogólnie oznaczają jakiś rodzaj błędu, ale sugerują różne rodzaje błędów. „Próbujesz użyć zerowego odwołania” często różni się bardzo od „niewłaściwie używasz biblioteki”.
EJoshuaS - Przywróć Monikę
7

Jest tak, ponieważ możliwe jest, że nie nastąpi to naturalnie. Zobaczmy taki kod:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(oczywiście w tej próbce jest wiele problemów z jakością kodu. Przykro mi, że wyobrażam sobie idealną próbkę)

Sytuacja, w której cnie będzie faktycznie używana i NullPointerExceptionnie zostanie wyrzucona, nawet jeśli c == nulljest to możliwe.

W bardziej skomplikowanych sytuacjach polowanie na takie przypadki staje się bardzo trudne. Właśnie dlatego ogólna kontrola jak if (c == null) throw new NullPointerException()jest lepsza.

Arenim
źródło
Prawdopodobnie kawałek kodu, który działa bez połączenia z bazą danych, gdy nie jest tak naprawdę potrzebny, jest dobrą rzeczą, a kod, który łączy się z bazą danych tylko po to, aby sprawdzić, czy nie może tego zrobić, jest zwykle dość denerwujący.
Dmitrij Grigoriew
5

Celowo należy chronić dalsze szkody lub doprowadzić do niespójności.

Fairoz
źródło
1

Oprócz wszystkich innych doskonałych odpowiedzi tutaj, chciałbym również dodać kilka przypadków.

Możesz dodać wiadomość, jeśli utworzysz własny wyjątek

Jeśli rzucisz własny NullPointerException, możesz dodać wiadomość (co zdecydowanie powinieneś!)

Komunikat domyślny jest to nullod new NullPointerException()a wszystkie metody, które go użyć, na przykład Objects.requireNonNull. Jeśli wydrukujesz tę wartość zerową, może ona nawet przekształcić się w pusty ciąg ...

Trochę krótki i mało pouczający ...

Śledzenie stosu da wiele informacji, ale aby użytkownik mógł dowiedzieć się, która wartość jest pusta, musi wykopać kod i przyjrzeć się dokładnemu wierszowi.

Teraz wyobraź sobie, że NPE jest pakowane i wysyłane przez sieć, np. Jako komunikat w błędzie usługi internetowej, być może między różnymi działami, a nawet organizacjami. W najgorszym przypadku nikt nie może zrozumieć, co nulloznacza ...

Połączone wywołania metod będą Cię zgadywać

Wyjątek powie ci tylko w jakim wierszu wystąpił wyjątek. Rozważ następujący wiersz:

repository.getService(someObject.someMethod());

Jeśli pojawi się NPE i wskazuje w tym samym wierszu, co jeden repositoryi someObjectbyła zerowa?

Zamiast tego, sprawdzenie tych zmiennych, gdy je otrzymasz, przynajmniej wskaże wiersz, w którym, mam nadzieję, jedyną obsługiwaną zmienną. Jak wspomniano wcześniej, nawet lepiej, jeśli komunikat o błędzie zawiera nazwę zmiennej lub podobną.

Błędy podczas przetwarzania dużej ilości danych wejściowych powinny zawierać informacje identyfikujące

Wyobraź sobie, że twój program przetwarza plik wejściowy z tysiącami wierszy i nagle pojawia się wyjątek NullPointerException. Patrzysz na to miejsce i zdajesz sobie sprawę, że niektóre dane wejściowe były nieprawidłowe ... jakie dane wejściowe? Potrzebujesz więcej informacji na temat numeru wiersza, być może kolumny lub nawet całego tekstu wiersza, aby zrozumieć, który wiersz w tym pliku wymaga naprawy.

Erk
źródło