Эффективная обработка исключений 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блоке не проверяет значение null перед попыткой доступа к методу Deque. Код во втором tryблоке проверяет наличие null и создает экземпляр реализации Deque(LinkedList), если он имеет значение null. Результат обоих примеров выглядит так:

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) 

Сообщение, следующее за ERROR в приведенных выше выходных данных, указывает на то, что NullPointerExceptionпри попытке вызова метода с нулевым значением создается сообщение Deque. Сообщение, следующее за INFO в выходных данных выше, указывает на то, что, Dequeсначала проверив значение null, а затем создав для него новую реализацию, когда оно равно null, исключение удалось полностью избежать.

Этот подход часто используется и, как показано выше, может быть очень полезным для предотвращения нежелательных (неожиданных) NullPointerExceptionслучаев. Однако это не обходится без затрат. Проверка на null перед использованием каждого объекта может привести к раздуванию кода, может быть утомительным для написания и открывает больше возможностей для проблем с разработкой и сопровождением дополнительного кода. По этой причине говорилось о введении поддержки языка Java для встроенного обнаружения null, автоматическом добавлении этих проверок на null после начального кодирования, нулевых безопасных типах, использовании аспектно-ориентированного программирования (AOP) для добавления проверки на null. в байтовый код и другие средства обнаружения нуля.

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

Поскольку проверка нулевого значения для каждой ссылки на объект может быть утомительной и приводит к раздуванию кода, многие разработчики предпочитают разумно выбирать, какие объекты проверять на нулевое значение. Обычно это приводит к проверке нуля для всех объектов потенциально неизвестного происхождения. Идея здесь в том, что объекты можно проверять на открытых интерфейсах, а затем считать их безопасными после начальной проверки.

Это ситуация, в которой тернарный оператор может быть особенно полезен. Вместо

// 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 только тогда, когда они поступают из «ненадежных» источников. Это часто означает проверку на null в первую очередь в методах, доступных для внешних вызывающих. Например, в конкретном классе разработчик может выбрать проверку на null для всех объектов, передаваемых publicметодам, но не проверять на null в 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. В качестве альтернативы, этот нулевой параметр можно было бы обработать разными способами. В случае, когда нулевой параметр не обрабатывался, не было никаких вариантов, как его обработать. Многие люди предпочитают выдавать a NullPolinterExceptionс дополнительной контекстной информацией, когда явно обнаруживается значение NULL (см. Элемент № 60 во втором издании Effective Java или элемент № 42 в первом издании), но я немного предпочитаю, IllegalArgumentExceptionкогда это явно аргумент метода имеет значение null, потому что я думаю, что само исключение добавляет подробности контекста, и легко включить "null" в тему.

Техника проверки аргументов метода на null на самом деле является подмножеством более общей техники проверки всех объектов на null. Однако, как указано выше, аргументы общедоступных методов часто являются наименее надежными в приложении, и поэтому их проверка может быть более важной, чем проверка среднего объекта на null.

Проверка параметров метода на нуль также является подмножеством более общей практики проверки параметров метода на общую валидность, как описано в элементе №38 Второго издания эффективной Java (элемент 23 в первом издании).

Рассматривайте примитивы, а не объекты

Я не думаю, что это хорошая идея выбирать примитивный тип данных (например, int) вместо соответствующего ему ссылочного типа объекта (например, Integer), просто чтобы избежать возможности a NullPointerException, но нельзя отрицать, что одно из преимуществ примитивные типы в том, что они не приводят к NullPointerExceptions. Однако примитивы по-прежнему необходимо проверять на достоверность (месяц не может быть отрицательным целым числом), поэтому это преимущество может быть небольшим. С другой стороны, примитивы не могут использоваться в коллекциях Java, и бывают случаи, когда требуется возможность установить значение null.

Самое главное - очень осторожно подходить к сочетанию примитивов, ссылочных типов и автобоксов. В Эффективной Java (Второе издание, пункт 49) есть предупреждение об опасностях, включая выброс NullPointerException, связанных с неосторожным смешиванием примитивных и ссылочных типов.

Тщательно продумайте вызовы связанных методов

NullPointerExceptionНайти A очень легко, потому что в номере строки будет указано, где оно произошло. Например, трассировка стека может выглядеть так, как показано ниже:

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

Трассировка стека делает очевидным, что NullPointerExceptionошибка возникла в результате выполнения кода в строке 222 файла AvoidingNullPointerExamples.java. Даже с предоставленным номером строки все еще может быть трудно сузить круг, какой объект является нулевым, если в одной строке есть несколько объектов с методами или полями, к которым осуществляется доступ.

For example, a statement like someObject.getObjectA().getObjectB().getObjectC().toString(); has four possible calls that might have thrown the NullPointerException attributed to the same line of code. Using a debugger can help with this, but there may be situations when it is preferable to simply break the above code up so that each call is performed on a separate line. This allows the line number contained in a stack trace to easily indicate which exact call was the problem. Furthermore, it facilitates explicit checking each object for null. However, on the downside, breaking up the code increases the line of code count (to some that's a positive!) and may not always be desirable, especially if one is certain none of the methods in question will ever be null.

Make NullPointerExceptions More Informative

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был пойман и повторно брошен с дополнительной контекстной информацией в виде 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