Эта простая статья скорее для начинающих разработчиков Java, хотя я нередко вижу и опытных коллег, которые беспомощно глядят на stack trace, сообщающий о NullPointerException (сокращённо NPE), и не могут сделать никаких выводов без отладчика. Разумеется, до NPE своё приложение лучше не доводить: вам помогут null-аннотации, валидация входных параметров и другие способы. Но когда пациент уже болен, надо его лечить, а не капать на мозги, что он ходил зимой без шапки.
Итак, вы узнали, что ваше приложение упало с NPE, и у вас есть только stack trace. Возможно, вам прислал его клиент, или вы сами увидели его в логах. Давайте посмотрим, какие выводы из него можно сделать.
NPE может произойти в трёх случаях:
Во втором и третьем случае message в объекте исключения всегда null, в первом может быть произвольным. К примеру, java.lang.System.setProperty кидает NPE с сообщением «key can't be null», если вы передали в качестве key null. Если вы каждый входной параметр своих методов проверяете таким же образом и кидаете исключение с понятным сообщением, то вам остаток этой статьи не потребуется.
Обращение по null-ссылке может произойти в следующих случаях:
Важно понимать, что эти случаи должны произойти именно в той строчке, на которой заканчивается 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 будет указывать на строку с вызовом.
В заключение хочется пожелать пореже запускать отладчик: после некоторой тренировки анализ кода в голове нередко выполняется быстрее, чем воспроизведение трудноуловимой ситуации.
Итак, вы узнали, что ваше приложение упало с 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, а не где-либо ещё.
Рассмотрим такой код:
1: class Data {
2: private String val;
3: public Data(String val) {this.val = val;}
4: public String getValue() {return val;}
5: }
6:
7: class Formatter {
8: public static String format(String value) {
9: return value.trim();
10: }
11: }
12:
13: public class TestNPE {
14: public static String handle(Formatter f, Data d) {
15: return f.format(d.getValue());
16: }
17: }
Откуда-то был вызван метод handle с какими-то параметрами, и вы получили:
Exception in thread "main" java.lang.NullPointerException
at TestNPE.handle(TestNPE.java:15)
В чём причина исключения — в f, d или d.val? Нетрудно заметить, что f в этой строке вообще не читается, так как метод format статический. Конечно, обращаться к статическому методу через экземпляр класса плохо, но такой код встречается (мог, например, появиться после рефакторинга). Так или иначе значение f не может быть причиной исключения. Если бы d был не null, а d.val — null, тогда бы исключение возникло уже внутри метода format (в девятой строчке). Аналогично проблема не могла быть внутри метода getValue, даже если бы он был сложнее. Раз исключение в пятнадцатой строчке, остаётся одна возможная причина: null в параметре d.
Вот другой пример:
1: class Formatter {
2: public String format(String value) {
3: return "["+value+"]";
4: }
5: }
6:
7: public class TestNPE {
8: public static String handle(Formatter f, String s) {
9: if(s.isEmpty()) {
10: return "(none)";
11: }
12: return f.format(s.trim());
13: }
14: }
Снова вызываем метод handle и получаем
Exception in thread "main" java.lang.NullPointerException
at TestNPE.handle(TestNPE.java:12)
Теперь метод format нестатический, и f вполне может быть источником ошибки. Зато s не может быть ни под каким соусом: в девятой строке уже было обращение к s. Если бы s было null, исключение бы случилось в девятой строке. Просмотр логики кода перед исключением довольно часто помогает отбросить некоторые варианты.
С логикой, конечно, надо быть внимательным. Предположим, условие в девятой строчке было бы написано так:
if("".equals(s))
Теперь в самой строчке обращения к полям и методам s нету, а метод equals корректно обрабатывает null, возвращая false, поэтому в таком случае ошибку в двенадцатой строке мог вызвать как f, так и s. Анализируя вышестоящий код, уточняйте в документации или исходниках, как используемые методы и конструкции реагируют на null. Оператор конкатенации строк +, к примеру, никогда не вызывает NPE.
Вот такой код (здесь может играть роль версия Java, я использую Oracle JDK 1.7.0.45):
1: import java.io.PrintWriter;
2:
3: public class TestNPE {
4: public static void dump(PrintWriter pw, MyObject obj) {
5: pw.print(obj);
6: }
7: }
Вызываем метод dump, получаем такое исключение:
Exception in thread "main" java.lang.NullPointerException
at java.io.PrintWriter.write(PrintWriter.java:473)
at java.io.PrintWriter.print(PrintWriter.java:617)
at TestNPE.dump(TestNPE.java:5)
В параметре pw не может быть null, иначе нам не удалось бы войти в метод print. Возможно, null в obj? Легко проверить, что pw.print(null) выводит строку «null» без всяких исключений. Пойдём с конца. Исключение случилось здесь:
472: public void write(String s) {
473: write(s, 0, s.length());
474: }
В строке 473 возможна только одна причина NPE: обращение к методу length строки s. Значит, s содержит null. Как так могло получиться? Поднимемся по стеку выше:
616: public void print(Object obj) {
617: write(String.valueOf(obj));
618: }
В метод write передаётся результат вызова метода String.valueOf. В каком случае он может вернуть null?
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
Единственный возможный вариант — obj не null, но obj.toString() вернул null. Значит, ошибку надо искать в переопределённом методе toString() нашего объекта MyObject. Заметьте, в stack trace MyObject вообще не фигурировал, но проблема именно там. Такой несложный анализ может сэкономить кучу времени на попытки воспроизвести ситуацию в отладчике.
Не стоит забывать и про коварный автобоксинг. Пусть у нас такой код:
1: public class TestNPE {
2: public static int getCount(MyContainer obj) {
3: return obj.getCount();
4: }
5: }
И такое исключение:
Exception in thread "main" java.lang.NullPointerException
at TestNPE.getCount(TestNPE.java:3)
На первый взгляд единственный вариант — это null в параметре obj. Но следует взглянуть на класс MyContainer:
import java.util.List;
public class MyContainer {
List<String> elements;
public MyContainer(List<String> elements) {
this.elements = elements;
}
public Integer getCount() {
return elements == null ? null : elements.size();
}
}
Мы видим, что getCount() возвращает Integer, который автоматически превращается в int именно в третьей строке TestNPE.java, а значит, если getCount() вернул null, произойдёт именно такое исключение, которое мы видим. Обнаружив класс, подобный классу MyContainer, посмотрите в истории системы контроля версий, кто его автор, и насыпьте ему крошек под одеяло.
Помните, что если метод принимает параметр int, а вы передаёте Integer null, то анбоксинг случится до вызова метода, поэтому NPE будет указывать на строку с вызовом.
В заключение хочется пожелать пореже запускать отладчик: после некоторой тренировки анализ кода в голове нередко выполняется быстрее, чем воспроизведение трудноуловимой ситуации.