Efektywna obsługa wyjątków Java NullPointerException

Nie potrzeba wiele doświadczenia w programowaniu w Javie, aby dowiedzieć się z pierwszej ręki, o czym jest wyjątek NullPointerException. W rzeczywistości jedna osoba podkreśliła, że ​​radzenie sobie z tym jest głównym błędem, jaki popełniają programiści Java. Wcześniej pisałem na blogu o używaniu String.value (Object) w celu zmniejszenia niechcianych wyjątków NullPointerExceptions. Istnieje kilka innych prostych technik, których można użyć w celu zmniejszenia lub wyeliminowania występowania tego powszechnego typu wyjątku RuntimeException, który był używany od JDK 1.0. W tym poście na blogu zebrano i podsumowano niektóre z najpopularniejszych z tych technik.

Przed użyciem sprawdź, czy każdy obiekt ma wartość Null

Najpewniejszym sposobem uniknięcia wyjątku NullPointerException jest sprawdzenie wszystkich odwołań do obiektów, aby upewnić się, że nie mają wartości NULL, przed uzyskaniem dostępu do jednego z pól lub metod obiektu. Jak pokazuje poniższy przykład, jest to bardzo prosta technika.

final String causeStr = "adding String to Deque that is set to null."; final String elementStr = "Fudd"; Deque deque = null; try { deque.push(elementStr); log("Successful at " + causeStr, System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { if (deque == null) { deque = new LinkedList(); } deque.push(elementStr); log( "Successful at " + causeStr + " (by checking first for null and instantiating Deque implementation)", System.out); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } 

W powyższym kodzie używany Deque jest celowo inicjowany do wartości null, aby ułatwić przykład. Kod w pierwszym trybloku nie sprawdza wartości null przed próbą uzyskania dostępu do metody Deque. Kod w drugim trybloku sprawdza wartość null i tworzy wystąpienie implementacji Deque(LinkedList), jeśli ma wartość null. Dane wyjściowe z obu przykładów wyglądają następująco:

ERROR: NullPointerException encountered while trying to adding String to Deque that is set to null. java.lang.NullPointerException INFO: Successful at adding String to Deque that is set to null. (by checking first for null and instantiating Deque implementation) 

Komunikat następujący po ERROR w danych wyjściowych powyżej wskazuje, że NullPointerExceptionzostanie zgłoszony, gdy zostanie podjęta próba wywołania metody dla wartości null Deque. Komunikat następujący po INFO w powyższym wyjściu wskazuje, że sprawdzając Dequenajpierw wartość null, a następnie tworząc dla niej nową implementację, gdy ma wartość null, wyjątek został całkowicie pominięty.

Takie podejście jest często używane i, jak pokazano powyżej, może być bardzo przydatne w unikaniu niechcianych (nieoczekiwanych) NullPointerExceptionprzypadków. Jednak nie jest to pozbawione kosztów. Sprawdzanie wartości null przed użyciem każdego obiektu może spowodować nadużywanie kodu, może być żmudne pisanie i otwiera więcej miejsca na problemy z opracowywaniem i utrzymaniem dodatkowego kodu. Z tego powodu mówi się o wprowadzeniu obsługi języka Java do wbudowanego wykrywania wartości null, automatycznym dodawaniu tych sprawdzeń pod kątem wartości null po początkowym kodowaniu, typach bezpiecznych dla wartości null, zastosowaniu programowania zorientowanego na aspekty (AOP) w celu dodania sprawdzania wartości null do kodu bajtowego i innych narzędzi do wykrywania wartości zerowych.

Groovy już zapewnia wygodny mechanizm radzenia sobie z odwołaniami do obiektów, które są potencjalnie zerowe. Operator bezpiecznej nawigacji Groovy'ego ( ?.) zwraca wartość null, a nie rzuca a, NullPointerExceptiongdy uzyskuje się dostęp do odwołania do obiektu o wartości null.

Ponieważ sprawdzanie wartości null dla każdego odwołania do obiektu może być żmudne i nadużywa kodu, wielu programistów decyduje się na rozważne wybieranie obiektów do sprawdzenia pod kątem wartości null. Zwykle prowadzi to do sprawdzenia wartości null na wszystkich obiektach o potencjalnie nieznanym pochodzeniu. Pomysł polega na tym, że obiekty można sprawdzać na odsłoniętych interfejsach, a następnie po wstępnym sprawdzeniu przyjąć, że są bezpieczne.

Jest to sytuacja, w której operator trójskładnikowy może być szczególnie przydatny. Zamiast

// retrieved a BigDecimal called someObject String returnString; if (someObject != null) { returnString = someObject.toEngineeringString(); } else { returnString = ""; } 

operator trójskładnikowy obsługuje tę bardziej zwięzłą składnię

// retrieved a BigDecimal called someObject final String returnString = (someObject != null) ? someObject.toEngineeringString() : ""; } 

Sprawdź argumenty metody pod kątem wartości Null

Omawianą technikę można zastosować na wszystkich obiektach. Jak stwierdzono w opisie tej techniki, wielu programistów decyduje się na sprawdzanie obiektów pod kątem wartości null tylko wtedy, gdy pochodzą z „niezaufanych” źródeł. Często oznacza to testowanie null jako pierwszej rzeczy w metodach udostępnianych zewnętrznym obiektom wywołującym. Na przykład w określonej klasie deweloper może wybrać sprawdzanie wartości null we wszystkich obiektach przekazanych do publicmetod, ale nie sprawdzanie wartości null w privatemetodach.

Poniższy kod demonstruje to sprawdzanie wartości null we wpisie metody. Zawiera pojedynczą metodę jako metodę demonstracyjną, która odwraca się i wywołuje dwie metody, przekazując każdej z nich pojedynczy pusty argument. Jedna z metod otrzymujących argument o wartości null sprawdza najpierw ten argument pod kątem wartości null, ale druga zakłada po prostu, że przekazany parametr nie ma wartości null.

 /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. * @throws IllegalArgumentException Thrown if the provided StringBuilder is * null. */ private void appendPredefinedTextToProvidedBuilderCheckForNull( final StringBuilder builder) { if (builder == null) { throw new IllegalArgumentException( "The provided StringBuilder was null; non-null value must be provided."); } builder.append("Thanks for supplying a StringBuilder."); } /** * Append predefined text String to the provided StringBuilder. * * @param builder The StringBuilder that will have text appended to it; should * be non-null. */ private void appendPredefinedTextToProvidedBuilderNoCheckForNull( final StringBuilder builder) { builder.append("Thanks for supplying a StringBuilder."); } /** * Demonstrate effect of checking parameters for null before trying to use * passed-in parameters that are potentially null. */ public void demonstrateCheckingArgumentsForNull() { final String causeStr = "provide null to method as argument."; logHeader("DEMONSTRATING CHECKING METHOD PARAMETERS FOR NULL", System.out); try { appendPredefinedTextToProvidedBuilderNoCheckForNull(null); } catch (NullPointerException nullPointer) { log(causeStr, nullPointer, System.out); } try { appendPredefinedTextToProvidedBuilderCheckForNull(null); } catch (IllegalArgumentException illegalArgument) { log(causeStr, illegalArgument, System.out); } } 

Po wykonaniu powyższego kodu dane wyjściowe wyglądają jak pokazano poniżej.

ERROR: NullPointerException encountered while trying to provide null to method as argument. java.lang.NullPointerException ERROR: IllegalArgumentException encountered while trying to provide null to method as argument. java.lang.IllegalArgumentException: The provided StringBuilder was null; non-null value must be provided. 

W obu przypadkach zarejestrowano komunikat o błędzie. Jednak przypadek, w którym sprawdzono wartość null, wywołał ogłoszony wyjątek IllegalArgumentException, który zawierał dodatkowe informacje kontekstowe o tym, kiedy napotkano wartość null. Alternatywnie, ten parametr zerowy mógłby zostać obsłużony na różne sposoby. W przypadku, gdy parametr null nie został obsłużony, nie było możliwości jego obsługi. Wiele osób woli wrzucić a NullPolinterExceptionz dodatkowymi informacjami kontekstowymi, gdy wartość null zostanie jawnie odkryta (patrz punkt 60 w drugim wydaniu efektywnej Java lub punkt 42 w wydaniu pierwszym), ale ja mam niewielkie preferencje, IllegalArgumentExceptiongdy jest to jawnie argument metody, który ma wartość null, ponieważ myślę, że sam wyjątek dodaje szczegóły kontekstu i łatwo jest zawrzeć „null” w temacie.

Technika sprawdzania argumentów metod pod kątem wartości null jest w rzeczywistości podzbiorem bardziej ogólnej techniki sprawdzania wszystkich obiektów pod kątem wartości null. Jednak, jak opisano powyżej, argumenty metod udostępnianych publicznie są często najmniej zaufane w aplikacji, dlatego ich sprawdzenie może być ważniejsze niż sprawdzenie, czy wartość null w przeciętnym obiekcie jest wartością zerową.

Sprawdzanie parametrów metody pod kątem wartości null jest również podzbiorem bardziej ogólnej praktyki sprawdzania parametrów metod pod kątem ogólnej ważności, jak omówiono w punkcie 38 drugiego wydania efektywnej Java (punkt 23 w pierwszym wydaniu).

Rozważmy raczej prymitywy niż obiekty

Nie sądzę, aby dobrym pomysłem było wybranie pierwotnego typu danych (takiego jak int) zamiast odpowiadającego mu typu odniesienia do obiektu (takiego jak Integer), aby po prostu uniknąć możliwości a NullPointerException, ale nie można zaprzeczyć, że jedna z zalet typy prymitywne są takie, że nie prowadzą do NullPointerExceptions. Jednak prymitywy nadal muszą być sprawdzane pod kątem ważności (miesiąc nie może być ujemną liczbą całkowitą), więc ta korzyść może być niewielka. Z drugiej strony, prymitywy nie mogą być używane w kolekcjach Java i czasami trzeba mieć możliwość ustawienia wartości na null.

Najważniejsze jest, aby zachować ostrożność podczas łączenia prymitywów, typów referencyjnych i autoboxingu. W Effective Java (wydanie drugie, pozycja nr 49) znajduje się ostrzeżenie dotyczące niebezpieczeństw, w tym rzucania NullPointerException, związanych z nieostrożnym mieszaniem typów pierwotnych i referencyjnych.

Ostrożnie rozważ wywołania metod łańcuchowych

A NullPointerExceptionmoże być bardzo łatwe do znalezienia, ponieważ numer linii wskazuje, gdzie wystąpił. Na przykład ślad stosu może wyglądać tak, jak pokazano poniżej:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(AvoidingNullPointerExamples.java:222) at dustin.examples.AvoidingNullPointerExamples.main(AvoidingNullPointerExamples.java:247) 

Ślad stosu pokazuje, że NullPointerExceptionzostał wyrzucony w wyniku kodu wykonanego w linii 222 programu AvoidingNullPointerExamples.java. Nawet przy podanym numerze wiersza nadal może być trudno określić, który obiekt jest pusty, jeśli istnieje wiele obiektów z metodami lub polami dostępnymi w tym samym wierszu.

Na przykład instrukcja taka jak someObject.getObjectA().getObjectB().getObjectC().toString();ma cztery możliwe wywołania, które mogły spowodować wyrzucenie NullPointerExceptionatrybutu przypisanego do tej samej linii kodu. Użycie debugera może w tym pomóc, ale mogą wystąpić sytuacje, w których lepiej po prostu podzielić powyższy kod, tak aby każde wywołanie było wykonywane w osobnym wierszu. Umożliwia to numerowi linii zawartej w śladzie stosu łatwe wskazanie, które dokładne wywołanie było przyczyną problemu. Ponadto ułatwia jawne sprawdzanie każdego obiektu pod kątem wartości null. Jednak z drugiej strony, rozbicie kodu zwiększa liczbę wierszy kodu (dla niektórych jest to dodatnie!) I nie zawsze może być pożądane, zwłaszcza jeśli jest się pewnym, że żadna z omawianych metod nigdy nie będzie zerowa.

Spraw, aby wyjątki NullPointerExceptions były bardziej informacyjne

In the above recommendation, the warning was to consider carefully use of method call chaining primarily because it made having the line number in the stack trace for a NullPointerException less helpful than it otherwise might be. However, the line number is only shown in a stack trace when the code was compiled with the debug flag turned on. If it was compiled without debug, the stack trace looks like that shown next:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace(Unknown Source) at dustin.examples.AvoidingNullPointerExamples.main(Unknown Source) 

Jak pokazuje powyższe dane wyjściowe, istnieje nazwa metody, ale nie ma numeru wiersza dla NullPointerException. Utrudnia to natychmiastowe zidentyfikowanie, co w kodzie doprowadziło do wyjątku. Jednym ze sposobów rozwiązania tego problemu jest podanie informacji kontekstowych w każdym wyrzuceniu NullPointerException. Pomysł ten został zademonstrowany wcześniej, gdy NullPointerExceptionzostał przechwycony i ponownie wyrzucony z dodatkowymi informacjami kontekstowymi jako plik IllegalArgumentException. Jednak nawet jeśli wyjątek zostanie po prostu ponownie zgłoszony jako inny NullPointerExceptionz informacjami kontekstowymi, nadal jest pomocny. Informacje kontekstowe pomagają osobie debugującej kod w szybszym zidentyfikowaniu prawdziwej przyczyny problemu.

Poniższy przykład ilustruje tę zasadę.

final Calendar nullCalendar = null; try { final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } try { if (nullCalendar == null) { throw new NullPointerException("Could not extract Date from provided Calendar"); } final Date date = nullCalendar.getTime(); } catch (NullPointerException nullPointer) { log("NullPointerException with useful data", nullPointer, System.out); } 

Wynik uruchomienia powyższego kodu wygląda następująco.

ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException ERROR: NullPointerException encountered while trying to NullPointerException with useful data java.lang.NullPointerException: Could not extract Date from provided Calendar