Как понять NullPointerException

    Эта простая статья скорее для начинающих разработчиков Java, хотя я нередко вижу и опытных коллег, которые беспомощно глядят на stack trace, сообщающий о NullPointerException (сокращённо NPE), и не могут сделать никаких выводов без отладчика. Разумеется, до NPE своё приложение лучше не доводить: вам помогут null-аннотации, валидация входных параметров и другие способы. Но когда пациент уже болен, надо его лечить, а не капать на мозги, что он ходил зимой без шапки.

    Итак, вы узнали, что ваше приложение упало с NPE, и у вас есть только stack trace. Возможно, вам прислал его клиент, или вы сами увидели его в логах. Давайте посмотрим, какие выводы из него можно сделать.

    NPE может произойти в трёх случаях:
    1. Его кинули с помощью throw
    2. Кто-то кинул null с помощью throw
    3. Кто-то пытается обратиться по null-ссылке

    Во втором и третьем случае message в объекте исключения всегда null, в первом может быть произвольным. К примеру, java.lang.System.setProperty кидает NPE с сообщением «key can't be null», если вы передали в качестве key null. Если вы каждый входной параметр своих методов проверяете таким же образом и кидаете исключение с понятным сообщением, то вам остаток этой статьи не потребуется.

    Обращение по null-ссылке может произойти в следующих случаях:
    1. Вызов нестатического метода класса
    2. Обращение (чтение или запись) к нестатическому полю
    3. Обращение (чтение или запись) к элементу массива
    4. Чтение length у массива
    5. Неявный вызов метода 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 будет указывать на строку с вызовом.

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

    Похожие публикации

    Комментарии 36
      +7
      Ещё очень популярное место возникновения NPE — при chaining-е методов, типа db.getUser().getFriends().first().getName(), которое возникает через мысли программиста типа «ну тут Null вернуться не должен.»
        0
        Описанные в посте NPE тривиальны и их причина находится очень быстро и просто. А вот то, что описали Вы как раз и приводит к ситуации «не могут сделать никаких выводов без отладчика».
          0
          Лезешь в базу и сразу всё становится понятно — надо оторвать руки тому, кто написал такой код — ни проверки на наличие собственно юзера, ни проверки на наличие у юзера френдов…
            0
            Такое решается использованием подхода «без null». К примеру, если getFriends() возвращает Friends ( т.е. какая-то коллекция Friend ), то всегда вместо null можно вернуть static final Friends EMPTY = new Friends(); т.е. можно все еще вызывать методы, просто коллекция пустая. Другие вызовы подобным подходом обвернуть.
              0
              getFriends ещё как-то может вернуть пустой список. В общем случае это даже более ожидаемое поведение (хотя гляньте-ка на метод Request.getCookies()).
              И даже для getUser для некоторой бизнес-логики может возвращать не null (например какого-нибудь guest'а). Но только если бизнес-логика рассчитывает увидеть какого-то дефолтного юзера. Если нет — полюбому должен быть признак отсутствия, а это null и NPE в случае такого чейнинга.

              А вот метод first по контракту названия и контекста однозначно должен попытаться зарезолвить первый элемент итератора, обнаружить отсутствие и свалиться с IllegalStateException. Согласен, это не NPE, но недалеко уехали — без проверок так делать нельзя. В Java. Для других языков могут быть приняты свои соглашения.
          0
          IMHO, chaining необходимо запрещать на уровне code style, и требовать присваивать отдельным переменным.
          Кстати, это и код делает более читабельным в 50% случаев, т.к. по коду одного и того же блока часто раскиданы одинаковые chaining-и вида getTable().getCurrent().getValue().
          +1
          Мне одному кажется, что такой код рефакторить надо? Одна строка == одно действие.
          Иначе читабельность же резко падает. И растёт ошибкоёмкость кода.
            –4
            А ещё лучше сразу всё правильно и красиво писать, чтобы никаких NPE не возникало.
              0
              Нормально читается, а проверки все можно встроить внутри методов и в случае если нет друзей или имени (да чего угодно) — бросать соответствующий Exception.

              В результате, вы напишете логичную и понятную 1 строчку, а все кейсы-исключения будут вынесены в отдельные блоки, таким образом логика не размазывается.

              PS/ Сам php-шник, но думаю, что тут подходы могут быть едины.
                0
                См. коммент выше.
                Если в chaining упал NPE, то очень трудно однозначно интерпретировать причину.
                Количество причин равно длине chain'а, и в итоге нужно анализировать гораздо больше сценариев.
                Не проще ли написать несколько операторов присваивания, и точно знать причину?
                  0
                  В случае именно с NPE я понимаю, что цепочные запросы очень сложны в расшифровке.

                  Но если предположить, что метод всегда возвращает правильный результат, либо бросает Exception — то цепочные запросы не должны быть настолько плохи.

                  Таким образом:

                  1. Если логику и проверки перенести в исходный метод и они написаны корректно (покрыты тестами) — цепочные запросы будут использоваться правильно и легко читаемы.
                  2. Если метод будет возвращать непроверенный результат — это может вызывать NPE и тут цепочные запросы могут усложнить жизнь.

                  И вот в первом описанном случае я все еще не вижу проблем.
              –7
              Отличная статья, жаль не могу плюсануть вас, кармы не хватает. Она мне конечно не сильно помогла, но все же. Как раз сейчас заканчиваю курс Java и помню, как в начале наступал на грабли по NPE. :) Сейчас слава богу появилась привычка чекать все variables на null перед передачи ее, как аргумента. А сколько раз я парился из-за проблемы при сравнении двух стрингов по типу (string1==string2) Я ошибку искал дня два наверное) Потом узнал про .equals() Веселая она Java.
                0
                Перед тем, как писать что-то адекватное, нужно прочитать не менее адекватный учебник по языку.
                В каждом языке свои тонкости, без знания которых гарантировано напишешь 100-500 багов.
                0
                Интересно, что сообщество скажет по теме — кидать NPE или не кидать. Или кидать другое исключение? У нас в офисе как-то целый холивар разгорелся. Вот я в коде компонента системы получил обьект, который на проверку оказался null. Возможности поправить вызывающий код с тем, чтобы там бросить специфичное для компонента исключение нет, например, вызовов хренова гора. Я в таких случаях кидаю NPE и пишу сообщение в лог. Как делаете вы?
                  –1
                  Вот статья для джунов про логгирование и исключения от 2006 года

                  today.java.net/article/2006/04/04/exception-handling-antipatterns#logAndThrow
                    0
                    Проигнорирую ваших джунов, хочу заметить, что ваша ссылка не очень подходит к моему вопросу. Она говорит, что ты или кидаешь исключение, или пишешь в лог и обрабатываешь некорректное состояние. С этим никто не спорит, сам всегда так делал.

                    Просто в случае нарушения контракта на границе компонентов (допустим, мой не ожидает получения нулевого обьекта) сама по себе ситуация спорная. В лог я напишу как раз назойливое сообщение, которое рано или поздно заставит собраться программистов и решить, что со всем этим делать, мы даже субботники по таким сообщениям устраивали.

                    Вопрос в том — бросить NPE самому или позволить ему случиться естественным образом? Мы так и не пришли к соглашению
                      +2
                      У себя пришли к следующему:

                      Весь код условно делится на внутренний и внешний. Внутренний — я написал метод, я же его и вызываю. Тут никаких проверок на аргументы нет вообще. Внешний — как раз на границе компонентов, вот тут проверки уже есть. Но, в данном случае бросаю не NPE, а IllegalArgumentException. Имхо, ближе по смыслу получается.
                        0
                        з.ы. а вот «назойливое сообщение в лог» считаю абсолютно бесполезным, туда все равно никто не смотрит.
                          0
                          Тут, наверное, зависит от того, как поставлены дела в проекте. Я очень заботился о нашем саппорте (чем понятнее логи, тем меньше саппорт будет дергать программистов), и поэтому полиси в проекте было делать лог как можно более понятным (к тому же, смотреть больше и некуда на сервер сайде). С логами у нас обычно все было в порядке и любая странная надпись тут же регистрировалась саппортом с просьбой разобраться, что там такое.
                          0
                          С джунами эт я погорячился, да :) Неоднократно встречался с этим антипаттерном и адом в логах.

                          О бросании NPE холивар древний и каждый похоже выбирает для себя что-то свое.
                          stackoverflow.com/questions/3881/illegalargumentexception-or-nullpointerexception-for-a-null-parameter

                          Я сам NPE никогда не кидаю, кидаю IllegalArgument или IllegalState
                            +2
                            Пожалуй плюс в случае самостоятельного выбрасывания NPE может быть в том, что можно указать сообщение, точно идентифицирующее проблему, что позволит быстрее с ней справиться. В качестве альтернативы можно выбросить и IllegalArgumentException.
                              +1
                              Я обычно бросаю IllegalArgumentException в этом случае.
                                0
                                В java.util.* распространена практика кидания NPE.
                                ИМХО, вполне нормально, если в JavaDoc описаны ВСЕ возможные случаи получения такого NPE.
                                0
                                Тип исключения должен соответствовать уровню абстракции, на котором оно выкидывается.
                                Более того, код должен перехватывать и обрабатывать исключения с нижнего уровня, приводя их к должному уровню абстракции. Это верно для любых независимых компонентов.
                              0
                              Я не кидаю. Мне «приятней», когда NPE значит, что ошибка пришла от JVM от разыменования нулевого указателя. Меньше думать надо. С другой стороны больших проблем не вижу, да и в JDK NPE кидается в некоторых местах. Дело вкуса.

                              В вашем случае, если я правильно понял, разумней кидать что-то вроде ExternalSystemUnexpectedFailureException (ну или согласно вашей предметной области наименование). Не очень понял, что там с хреновой горой вызовов. Если речь про checked exception, то я сторонник избавления от них везде, где только возможно.
                                0
                                Ну вот товарищи предложили кидать IllegalArgumentException, что кажется подходящим. Хренова гора вызовов — это придуманное условие невозможности изменения кода вызова. Просто по хорошему, если я встречаю такую ситуацию, я правлю вызов так, чтобы ситуация с нулем обрабатывалась на той стороне и вызова к моему компоненту не приходило, и такая ситуация невозможна.
                                  0
                                  Плюсы checked exception понимаешь только тогда, когда тебе в production прилетел системный RuntimeException из недр framework'а с совершенно неадекватным сообщением об ошибке. :-)
                                  P.S. К сожалению, довольно частая ситуация во всяких решениях, построенных на SAX-парсерах.
                                    0
                                    Ну прилетит Checked Exception с неадекватным сообщением об ошибке. А скорее (если принять гипотезу о низком уровне разработчиков) этот Exception будет молча проглочен внутрях этого фреймворка и долго будешь голову ломать — почему вторая половина конфига не оказывает влияния на приложение.
                                0
                                А почему не рассмотрели следующую ситуацию? Это конечно упрощенно, но смысл думаю понятен. Раз статья для новичков, то думаю они с такой ситуацией сталкивались и могли не сразу понять от чего вдруг NPE.

                                public class Test {
                                public static void main(String[] args) {
                                Integer i = null;
                                // Тут какая-нибудь логика с i и null в итоге остается
                                test(i);
                                }

                                private static void test(int i) {
                                // Тут работаем с i
                                }
                                }

                                К сожалению тег source отработал не так как я себе представлял.
                                  0
                                  Я написал про этот случай:
                                  Помните, что если метод принимает параметр int, а вы передаёте Integer null, то анбоксинг случится до вызова метода, поэтому NPE будет указывать на строку с вызовом.

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

                                  Спасибо вам за то, что вы есть.
                                    0
                                    Зато s не может быть ни под каким соусом: в девятой строке уже было обращение к s. Если бы s было null, исключение бы случилось в девятой строке.

                                    В многопоточной среде это утверждение, строго говоря, неверно.
                                      +1
                                      Нет. Локальные переменные или параметры в Java невозможно изменить из другого потока.
                                        0
                                        Верно только за счет иммутабельности String'а :-)
                                          +2
                                          Почему? Моё высказывание верно всегда. Изменение полей объекта, на который ссылается локальная переменная, не есть изменение локальной переменной.

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое