Как понять 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
                                          Почему? Моё высказывание верно всегда. Изменение полей объекта, на который ссылается локальная переменная, не есть изменение локальной переменной.

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