Как понять 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, а не где-либо ещё.Рассмотрим такой код:

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 elements; public MyContainer (List 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 будет указывать на строку с вызовом.

В заключение хочется пожелать пореже запускать отладчик: после некоторой тренировки анализ кода в голове нередко выполняется быстрее, чем воспроизведение трудноуловимой ситуации.

© Habrahabr.ru