Ефективно обработване на Java NullPointerException

Не е необходим много опит в разработката на Java, за да научите от първа ръка какво е NullPointerException. Всъщност един човек подчерта, че се справя с това като грешка номер едно, която Java разработчиците правят. Блогирах преди това за използването на String.value (Object) за намаляване на нежеланите NullPointerExceptions. Има няколко други прости техники, които човек може да използва, за да намали или елиминира появата на този често срещан тип RuntimeException, който е с нас от JDK 1.0. Тази публикация в блога събира и обобщава някои от най-популярните от тези техники.

Проверете всеки обект за нула преди да използвате

Най-сигурният начин да се избегне NullPointerException е да се проверят всички препратки към обекти, за да се гарантира, че те не са нулеви, преди да се осъществи достъп до едно от полетата или методите на обекта. Както показва следващият пример, това е много проста техника.

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); } 

В горния код, използваният Deque е умишлено инициализиран, за да улесни примера. Кодът в първия tryблок не проверява за нула, преди да се опита да осъществи достъп до метод Deque. Кодът във втория tryблок проверява за нула и създава екземпляр на изпълнение на Deque(LinkedList), ако е нула. Резултатът от двата примера изглежда така:

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) 

Съобщението след ГРЕШКА в изхода по-горе показва, че NullPointerExceptionсе изхвърля а при опит за извикване на метод за нула Deque. Съобщението след INFO в изхода по-горе показва, че като първо се проверява Dequeза null и след това се създава инстанция за ново изпълнение за него, когато е null, изключението е избегнато изобщо.

Този подход често се използва и, както е показано по-горе, може да бъде много полезен за избягване на нежелани (неочаквани) NullPointerExceptionслучаи. Не е обаче и без разходите си. Проверката за нула преди използване на всеки обект може да издуе кода, да бъде досадно да се пише и да отвори повече място за проблеми с разработването и поддръжката на допълнителния код. Поради тази причина се говори за въвеждане на поддръжка на Java език за вградено откриване на нула, автоматично добавяне на тези проверки за нула след първоначалното кодиране, нулево безопасни типове, използване на Aspect-Oriented Programming (AOP) за добавяне на нулева проверка за байт код и други инструменти за откриване на нула.

Groovy вече предоставя удобен механизъм за работа с референтни обекти, които са потенциално нулеви. Операторът за безопасна навигация на Groovy ( ?.) връща null, вместо да изхвърля a, NullPointerExceptionкогато се осъществява достъп до справка за нулев обект.

Тъй като проверката на null за всеки референтен обект може да бъде досадна и да раздува кода, много разработчици избират разумно да изберат кои обекти да проверяват за null. Това обикновено води до проверка на null за всички обекти с потенциално неизвестен произход. Идеята тук е, че обектите могат да бъдат проверени на изложени интерфейси и след това да се приемат за безопасни след първоначалната проверка.

Това е ситуация, при която троичният оператор може да бъде особено полезен. Вместо

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

троичният оператор поддържа този по-кратък синтаксис

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

Проверете аргументите на метода за Null

Току-що обсъдената техника може да се използва за всички обекти. Както е посочено в описанието на тази техника, много разработчици избират да проверяват обектите за нула само когато идват от "ненадеждни" източници. Това често означава тестване за нула първо в методите, изложени на външни повикващи. Например, в определен клас, разработчикът може да избере да провери за нула за всички обекти, предадени на publicметоди, но не и за нула в privateметодите.

Следващият код демонстрира тази проверка за нула при въвеждане на метод. Той включва един метод като демонстративен метод, който се обръща и извиква два метода, предавайки на всеки метод по един нулев аргумент. Един от методите, получаващи нулев аргумент, първо проверява този аргумент за нулев, но другият просто приема, че предаденият параметър не е нулев.

 /** * 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); } } 

Когато горният код се изпълни, изходът се появява, както е показано по-нататък.

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. 

И в двата случая се регистрира съобщение за грешка. Случаят, в който е била проверена null, е хвърлил рекламирано IllegalArgumentException, което включва допълнителна контекстна информация за това кога е открита null. Като алтернатива, този нулев параметър би могъл да се обработва по различни начини. За случая, в който не се обработва нулев параметър, няма опции за това как да се обработи. Много хора предпочитат да хвърлят NullPolinterExceptionс допълнителна информация за контекста, когато е изрично открита нула (виж Елемент № 60 във Второто издание на Ефективна Java или Елемент № 42 в Първо издание), но аз имам леко предпочитание IllegalArgumentExceptionкога е изрично аргумент на метод, който е нулев, защото мисля, че самото изключение добавя подробности за контекста и е лесно да се включи "null" в темата.

Техниката за проверка на аргументите на метода за null е наистина подмножество на по-общата техника за проверка на всички обекти за null. Както е посочено по-горе, аргументите за публично изложени методи често са най-малко доверени в дадено приложение и затова тяхната проверка може да е по-важна от проверката на средния обект за нула.

Проверката на параметрите на метода за нула също е подмножество на по-общата практика за проверка на параметрите на метода за обща валидност, както е обсъдено в Елемент # 38 от Второто издание на Ефективна Java (Елемент 23 в Първо издание).

Помислете за примитиви, а не за обекти

Не мисля, че е добра идея да изберете примитивен тип данни (като int) над съответния референтен тип обект (като Integer), само за да се избегне възможността за a NullPointerException, но не може да се отрече, че едно от предимствата на примитивни типове е, че те не водят до NullPointerExceptions. Въпреки това примитивите все още трябва да бъдат проверени за валидност (един месец не може да бъде отрицателно цяло число) и така тази полза може да е малка. От друга страна, примитивите не могат да се използват в Java Collections и има моменти, когато човек иска възможността да зададе стойност на null.

Най-важното е да бъдете много предпазливи относно комбинацията от примитиви, референтни типове и автобокс. В Effective Java (Второ издание, позиция # 49) има предупреждение относно опасностите, включително хвърляне на NullPointerException, свързани с небрежно смесване на примитивни и референтни типове.

Внимателно обмислете повиквания с верижен метод

A NullPointerExceptionможе да бъде много лесно да се намери, защото номер на ред ще посочи къде е възникнал. Например проследяването на стека може да изглежда по следния начин:

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

Проследяването на стека става очевидно, че NullPointerExceptionе хвърлен в резултат на код, изпълнен на ред 222 от AvoidingNullPointerExamples.java. Дори с предоставения номер на ред, все още може да е трудно да се стесни кой обект е нулев, ако има множество обекти с методи или полета, достъпни на един и същ ред.

Например изявление като someObject.getObjectA().getObjectB().getObjectC().toString();има четири възможни повиквания, които може да са хвърлили NullPointerExceptionатрибута към същия ред код. Използването на дебъгер може да помогне за това, но може да има ситуации, когато е за предпочитане просто да разбиете горния код, така че всяко обаждане да се извършва на отделен ред. Това позволява номерът на реда, съдържащ се в стека на стека, лесно да посочи кое точно повикване е било проблем. Освен това улеснява изричната проверка на всеки обект за нула. Недостатъкът обаче е, че разбиването на кода увеличава броя на редовете на кода (за някои това е положително!) И може да не е винаги желателно, особено ако някой е сигурен, че нито един от въпросните методи никога няма да бъде нулев.

Направете NullPointerExceptions по-информативни

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) 

Както показва горният изход, има име на метод, но не и номер на ред за NullPointerException. Това затруднява незабавното идентифициране на това, което в кода доведе до изключението. Един от начините да се справим с това е да предоставим контекстна информация във всеки изхвърлен NullPointerException. Тази идея беше демонстрирана по-рано, когато a NullPointerExceptionбеше хванат и повторно хвърлен с допълнителна контекстна информация като a IllegalArgumentException. Въпреки това, дори ако изключението е просто прехвърлено като друго NullPointerExceptionс контекстна информация, то все пак е полезно. Контекстната информация помага на лицето, отстраняващо грешки в кода, да идентифицира по-бързо истинската причина за проблема.

Следващият пример демонстрира този принцип.

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); } 

Резултатът от изпълнението на горния код изглежда както следва.

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