Как понять NullPointerException
Эта простая статья скорее для начинающих разработчиков Java, хотя я нередко вижу и опытных коллег, которые беспомощно глядят на stack trace, сообщающий о NullPointerException (сокращённо NPE), и не могут сделать никаких выводов без отладчика. Разумеется, до NPE своё приложение лучше не доводить: вам помогут null-аннотации, валидация входных параметров и другие способы. Но когда пациент уже болен, надо его лечить, а не капать на мозги, что он ходил зимой без шапки.
Итак, вы узнали, что ваше приложение упало с NPE, и у вас есть только stack trace. Возможно, вам прислал его клиент, или вы сами увидели его в логах. Давайте посмотрим, какие выводы из него можно сделать.
NPE может произойти в трёх случаях:
- Его кинули с помощью throw
- Кто-то кинул null с помощью throw
- Кто-то пытается обратиться по null-ссылке
Во втором и третьем случае message в объекте исключения всегда null, в первом может быть произвольным. К примеру, java.lang.System.setProperty кидает NPE с сообщением «key can’t be null», если вы передали в качестве key null. Если вы каждый входной параметр своих методов проверяете таким же образом и кидаете исключение с понятным сообщением, то вам остаток этой статьи не потребуется.
Обращение по null-ссылке может произойти в следующих случаях:
- Вызов нестатического метода класса
- Обращение (чтение или запись) к нестатическому полю
- Обращение (чтение или запись) к элементу массива
- Чтение length у массива
- Неявный вызов метода valueOf при анбоксинге (unboxing)
Важно понимать, что эти случаи должны произойти именно в той строчке, на которой заканчивается stack trace, а не где-либо ещё.
Рассмотрим такой код:
Откуда-то был вызван метод handle с какими-то параметрами, и вы получили:
В чём причина исключения — в f, d или d.val? Нетрудно заметить, что f в этой строке вообще не читается, так как метод format статический. Конечно, обращаться к статическому методу через экземпляр класса плохо, но такой код встречается (мог, например, появиться после рефакторинга). Так или иначе значение f не может быть причиной исключения. Если бы d был не null, а d.val — null, тогда бы исключение возникло уже внутри метода format (в девятой строчке). Аналогично проблема не могла быть внутри метода getValue, даже если бы он был сложнее. Раз исключение в пятнадцатой строчке, остаётся одна возможная причина: null в параметре d.
Вот другой пример:
Снова вызываем метод handle и получаем
Теперь метод format нестатический, и f вполне может быть источником ошибки. Зато s не может быть ни под каким соусом: в девятой строке уже было обращение к s. Если бы s было null, исключение бы случилось в девятой строке. Просмотр логики кода перед исключением довольно часто помогает отбросить некоторые варианты.
С логикой, конечно, надо быть внимательным. Предположим, условие в девятой строчке было бы написано так:
Теперь в самой строчке обращения к полям и методам s нету, а метод equals корректно обрабатывает null, возвращая false, поэтому в таком случае ошибку в двенадцатой строке мог вызвать как f, так и s. Анализируя вышестоящий код, уточняйте в документации или исходниках, как используемые методы и конструкции реагируют на null. Оператор конкатенации строк +, к примеру, никогда не вызывает NPE.
Вот такой код (здесь может играть роль версия Java, я использую Oracle JDK 1.7.0.45):
Вызываем метод dump, получаем такое исключение:
В параметре pw не может быть null, иначе нам не удалось бы войти в метод print. Возможно, null в obj? Легко проверить, что pw.print(null) выводит строку «null» без всяких исключений. Пойдём с конца. Исключение случилось здесь:
В строке 473 возможна только одна причина NPE: обращение к методу length строки s. Значит, s содержит null. Как так могло получиться? Поднимемся по стеку выше:
В метод write передаётся результат вызова метода String.valueOf. В каком случае он может вернуть null?
Единственный возможный вариант — obj не null, но obj.toString() вернул null. Значит, ошибку надо искать в переопределённом методе toString() нашего объекта MyObject. Заметьте, в stack trace MyObject вообще не фигурировал, но проблема именно там. Такой несложный анализ может сэкономить кучу времени на попытки воспроизвести ситуацию в отладчике.
Не стоит забывать и про коварный автобоксинг. Пусть у нас такой код:
И такое исключение:
На первый взгляд единственный вариант — это null в параметре obj. Но следует взглянуть на класс MyContainer:
Мы видим, что getCount() возвращает Integer, который автоматически превращается в int именно в третьей строке TestNPE.java, а значит, если getCount() вернул null, произойдёт именно такое исключение, которое мы видим. Обнаружив класс, подобный классу MyContainer, посмотрите в истории системы контроля версий, кто его автор, и насыпьте ему крошек под одеяло.
Помните, что если метод принимает параметр int, а вы передаёте Integer null, то анбоксинг случится до вызова метода, поэтому NPE будет указывать на строку с вызовом.
В заключение хочется пожелать пореже запускать отладчик: после некоторой тренировки анализ кода в голове нередко выполняется быстрее, чем воспроизведение трудноуловимой ситуации.
How to Handle Null Pointer Exception in Java
In Java, a special null value can be assigned to an object’s reference and denotes that the object is currently pointing to unknown piece of data. A NullPointerException is thrown when an application is trying to use or access an object whose reference equals to null . The following cases throw that exception:
- Invoking a method from a null object.
- Accessing or modifying a null object’s field.
- Taking the length of null , as if it were an array.
- Accessing or modifying the slots of null object, as if it were an array.
- Throwing null , as if it were a Throwable value.
- When you try to synchronize over a null object.
The NullPointerException is a RuntimeException and thus, the Javac compiler does not force you to use a try-catch block to handle it appropriately.
Why do we need the null value?
As already mentioned, null is a special value used in Java. It is extremely useful in coding some design patterns, such as Null Object pattern and Singleton pattern. The Null Object pattern provides an object as a surrogate for the lack of an object of a given type. The Singleton pattern ensures that only one instance of a class is created and also, aims for providing a global point of access to the object.
For example, a sample way to create at most one instance of a class is to declare all its constructors as private and then, create a public method that returns the unique instance of the class:
In this example, we declare a static instance of the Singleton class. That instance is initialized at most once inside the getInstance method. Notice the use of the null value that enables the unique instance creation.
How to avoid the NullPointerException
In order to avoid the NullPointerException , ensure that all your objects are initialized properly, before you use them. Notice that, when you declare a reference variable, you are really creating a pointer to an object. You must verify that the pointer is not null, before you request the method or a field from the object.
Also, if the exception is thrown, use the information residing in the exception’s stack trace. The execution’s stack trace is provided by the JVM, in order to enable the debugging of the application. Locate the method and the line where the exception was caught and then, figure out which reference equals to null in that specific line.
In the rest of this section, we will describe some techniques that deal with the aforementioned exception. However, they do not eliminate the problem and the programmer should always be careful while coding an application.
1. String comparison with literals
A very common case in an application’s execution code involves the comparison between a String variable and a literal. The literal may be a String or the element of an Enum. Instead of invoking the method from the null object, consider invoking it from the literal. For example, observe the following case:
The above code snippet will throw a NullPointerException . However, if we invoke the method from the literal, the execution flow continues normally:
2. Check the arguments of a method
Before executing the body of your own method, be sure to check its arguments for null values. Continue with the execution of the method, only when the arguments are properly checked. Otherwise, you can throw an IllegalArgumentException and notify the calling method that something is wrong with the passed arguments.
3. Prefer String.valueOf() method instead of toString()
When your application’s code requires the String representation of an object, avoid using the object’s toString method. If your object’s reference equals to null , a NullPointerException will be thrown.
Instead, consider using the static String.valueOf method, which does not throw any exceptions and prints «null» , in case the function’s argument equals to null .
4. Use the Ternary Operator
The ternary operator can be very useful and can help us avoid the NullPointerException . The operator has the form:
First, the boolean expression is evaluated. If the expression is true then, the value1 is returned, otherwise, the value2 is returned. We can use the ternary operator for handling null pointers as follows:
The message variable will be empty if str ’s reference is null. Otherwise, if str points to actual data, the message will retrieve the first 10 characters of it.
5. Create methods that return empty collections instead of null
A very good technique is to create methods that return an empty collection, instead of a null value. Your application’s code can iterate over the empty collection and use its methods and fields, without throwing a NullPointerException . For example:
6. Make use of Apache’s StringUtils class
Apache’s Commons Lang is a library that provides helper utilities for the java.lang API, such as String manipulation methods. A sample class that provides String manipulation is StringUtils.java , which handles null input Strings quietly.
You can make use of the StringUtils.isNotEmpty, StringUtils.IsEmpty and StringUtils.equals methods, in order to avoid the NullPointerException . For example:
7. Use the contains(), containsKey(), containsValue() methods
If your application code makes use of collections, such as Maps , consider using the contains, containsKey and containsValue methods. For example, retrieve the value of a specific key, after you have verified its existence in the map:
In the above snippet, we don’t check if the key actually exists inside the Map and thus, the returned value can be null . The safest way is the following:
8. Check the return value of external methods
It is very common in practice to make use of external libraries. These libraries contain methods that return a reference. Make sure that the returned reference is not null . Also, consider reading the Javadoc of the method, in order to better understand its functionality and its return values.
9. Use Assertions
Assertions are very useful while testing your code and can be used, in order to avoid executing code snippets that will throw a NullPointerException . Java Assertions are implemented with the assert keyword and throw an AssertionError .
Notice that you must explicitly enable the assertion flag of the JVM, by executing it with the –ea argument. Otherwise, the assertions will be completely ignored.
A sample example using Java assertions is the following:
If you execute the above code snippet and pass a null argument to getLength , the following error message will appear:
Exception in thread «main» java.lang.AssertionError
Finally, you can use the Assert class provided by the jUnit testing framework.
10. Unit Tests
Unit tests can be extremely useful while testing the functionality and correctness of your code. Devote some time to write a couple tests cases that verify that no NullPointerException is thrown, while your application’s code undergoes a specific execution flow.
Existing NullPointerException safe methods
1. Accessing static members or methods of a class
When your code attempts to access a static variable or method of a class, even if the object’s reference equals to null , the JVM does not throw a NullPointerException . This is due to the fact that the Java compiler stores the static methods and fields in a special place, during the compilation procedure. Thus, the static fields and methods are not associated with objects, rather with the name of the class.
For example, the following code does not throw a NullPointerException :
Notice, that despite the fact that the instance of the SampleClass equals to null , the method will be executed properly. However, when it comes to static methods or fields, it is better to access them in a static way, such as SampleClass.printMessage() .
2. The instanceof operator
The instanceof operator can be used, even if the object’s reference equals to null . The instanceof operator returns false when the reference equals to null and does not throw a NullPointerException . For example consider the following code snippet:
The result of the execution is, as expected:
Helpful NullPointerExceptions
My professor once said “null pointer exceptions are the bane of Java programmers existence” and that never resonated quite as well as with enterprise application development. When the exception message offers no more than a line number, it can be difficult to identify the null reference and how to fix it.
Helpful NullPointerException messages to the rescue! This enhancement proposal will add more detail to null pointer exceptions helping developers debug code faster. It is scheduled to be released with JDK 14 in March 2020 and can be enabled with JVM flag -XX:+ShowCodeDetailsInExceptionMessages . Currently, early access builds of JDK 14 are available.
Method Invocation
We often chain methods in Java and when chaining throws a null pointer exception, it can be difficult to know where it came from. Suppose your code throws a NullPointerException with this line at the root cause.
Where is the problem? There are 4 ways this code can throw a null pointer exception. With helpful null pointer exceptions enabled, the message would read: Cannot invoke «Name.toFullName()» because the return value of «Dog.getName()» is null . With the additional information provided by the JVM, we know this exception was caused by a dog that does not have a name. This knowledge jump starts the debugging process for the developer and saves time.
Fields
Attempting to read or assign a field when the parent object is null will result in a null pointer exception. Let’s look at how Java is improving those messages too. Below we try to read then assign field dog but local variable human is null. The exception messages for each scenario are below the code.
Arrays
In this sample, we try to read from an array that is null. Without improved exception messages, it would be difficult to know which of the three array lookups threw the NullPointerException.
Similarly, we cannot store a value into the index of a null array.
Finally, the JVM handles accessing an array’s length a little different than other fields and the exception message is different as well.
Other Null Pointer Exceptions
A less common null pointer exception is when a null exception is thrown.
Perhaps even less common is the null pointer exception thrown by opening a synchronized block over a null object.
Limitations with Local Variables
Class files will include the names of all classes, methods, and fields used. That’s not always the case for local variables. Because the JVM does not need to know what local variables are called to execute the instructions, the compiler will only include them if requested. To include local variable names, the code should be compiled with the -g flag for additional debug information. Below is a sample exception message from code compiled without local variable names included in the class file.
Without variable names included in the class file, the JVM provides all it knows about the variable. The number in is the local variable’s index assigned by the compiler. It is the ordinality of the variable’s declaration within the method but ignoring any variables that are no longer in scope. That’s a pretty dense sentence, so here’s an example of how javac might assign indexes.
The JVM wants to use as few indexes as possible since each index reserves a memory address. A smaller max index means a smaller memory footprint on the call stack. When declaring variables a , b , and c , we assigning them the indexes 1, 2, and 3 based on the order they are declared. This pattern will continue with one caveat. When we declare variable d , there are only 2 variables in scope; that is variable c has left scope and its memory is no longer needed. For this reason we can assign d the index 3. When we get to the declaration of f , both d and e have left scope freeing their memory addresses. The next free index at this point is 3, so f becomes local3. While it may be a little tedious, we can deterministically know what any variable is given the original source code. Including debug information of original variable names is helpful but results in larger class files.
Conclusion
Improved null pointer exception messages help developers debug code faster by including more details about the source code that caused the exception. It is available in JDK 14 but must be enabled with VM argument -XX:+ShowCodeDetailsInExceptionMessages . However, there is a proposal to enable the feature by default in JDK 15 pending feedback from JDK 14.
Don’t just write code. Craft it.
© 2022 G. Hunter Anderson All rights reserved. | Styled with Coder by Luiz de Prá.