Что такое NullPointerException, почему возникает и как исправить?

Что из себя представляет исключение NullPointerExceptions (оно же java.lang.NullPointerException, или просто NPE/НПЕ). Как, в каких обстоятельствах и по каким причинам оно возникает.

Для правильного понимания сути это исключения нужно иметь представление о ссылочных типах в Java. Если объявляется переменная ссылочного типа (к таковым типам относятся массивы и, например, все объекты), то её непосредственное значение -- это всегда адрес какого-то объекта в куче. Такой адрес и называется ссылкой.

Так, в следующем примере переменная obj будет ссылаться на объект new Object(), созданный в куче (не забывайте, что оператор присваивания работает справа налево, так что сначала будет создан объект, и лишь потом произойдёт его присваивание объявленной переменной):

Object obj = new Object();

Но переменная obj также может ссылаться "в никуда", если её инициализировать значением null:

Object obj = null;

Инициализированная таким образом переменная obj не ссылается на конкретный объект в куче. Другими словами она не равна какому-либо конкретному адресу в памяти машины. Соответственно невозможно пройти по этому адресу, найти там объект и, к примеру, вызвать на нём метод:

obj.toString();

В приведённом примере мы пытаемся вызвать метод toString(), на переменной, которая указывает "в никуда". Что и приводит к возникновению исключительной ситуации и Java-машина выбрасывает NullPointerException.

Таким образом, NullPointerException -- это исключение, которое выбрасывает Java-машина, когда пытается разыменовать переменную или поле ссылочного типа в тот момент, когда данная переменная/поле не указывают ни на какой объект в куче, то есть не равны какому-либо адресу в памяти.

Выбрасывание NullPointerException Java-машиной

NullPointerException выбрасывается при следующих обстоятельствах:

  • вызов не статического метода на переменной или поле, которые равны null, то есть фактически не ссылаются ни на какой конкретный объект в куче:

Object obj = null;
obj.toString(); //NPE
  • чтение или запись в поле объекта, ссылка на который, на самом деле равна null:

class Model {
String name;
}
Model m = null;
String name = m.name; //NPE
m.name = "Kiss"; //NPE
  • использование оператора [] для доступа к элементу массива, если вместо ссылки на реальный массив окажется null:

int nums[] = null;int i = nums[1]; //NPE
  • использование поля length на массиве, если вместо ссылки на реальный массив окажется null:

int nums[] = null;int l = nums.length; //NPE
  • использование операторов сравнения (>, >=, <, <=) на переменных-обёртках над примитивными числовыми типами (Integer, Double и т.п.), если они окажутся равны null:

Integer num = null;boolean notOk = num > 0; //NPE
  • присваивание, сопряжённое с анбоксингом обёртки в примитив, когда значение переменной-обёртки равно null'у:

Integer j = null;int i = j; //NPE
  • попытка выбросить null в качестве исключения:

IllegalArgumentException ex = null;throw x; //NPE
  • попытка синхронизироваться на null'е:

class Example{ private Object obj = null; public void doSomething() { synchronized (obj) { //NPE } }}


NPE-безопасные операции

Следующие операции над переменными и полями классов равными null'у не приводят к возникновению NullPointerException:

  • Присваивание ссылочной переменной:

Object nullObj = null;Object testObj = nullObj;

Вторая строка данного примера не выбросит исключения, а testObj будет равнятся тому же, чему равен nullObj, то есть null'у.

  • Присваивание элементу массива (при условии, что переменная массива сама указывает на реальный массив) и чтение null'а из элемента массива:

Integer nums[] = new Integer[10];nums[0] = null;Integer first = nums[0];

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

  • Null может быть перед в качестве аргумента в метод и возвращён методом в качестве результата:

void testInvoke() { toNull(null);}
Object toNull(Object o) { return null;}

Метод toNull хотя и обещает вернуть Object, по коду мы видим, что он всегда возвращает null. Метод testInvoke вызывает метод toNull, передавая ему в качестве параметра null. Весь код отработает без выброса исключительных ситуаций, так как и передавать null в качестве параметра и возвращать его в качестве возвращаемого значения в Java можно.

  • проверка на равенство/неравенство (== и !=), а также использование null'а или равной null'у переменной в качестве левого операнда оператора instanceof:

Integer i = null;if(i == null) {}else if (i != null) {}if(i instanceof Integer){}

В данном примере все проверки отработают без ошибок. Даже такой код: if(null instanceof Integer){} не выбросит исключения.


Типичные ситуации

Самыми простыми случаями можно считать ситуации, когда ссылки инициализируются null'ами автоматически.

Во-первых, это инициализация null'ами элементов массива:

Integer nums[] = new Integer[10];System.out.println(nums[0]);

Вторая строка выведет null, так как все 10 элементов массива nums автоматически инициализированы null'ами (метод System.out.println() получив на вход null, не выбрасывает исключение, а печатает в консоль строку "null\n"). Но если после такой автоматической инициализации записать реальные данные не во все 10 ячеек массива, а потом в цикле for перебрать их все, то есть большой шанс получить NPE.

Во-вторых, это инициализация ссылочных полей классов:

public class Model { Long id;}

Поле id будет автоматически инициализировано null'ом при создании каждого объекта Model. Им можно будет пользоваться как любой инициализированной переменной. Но нужно помнить, что по умолчанию оно равно null.

Кроме того, возникновение данного исключения часто может быть неочевидно. Например:

public boolean isLongString(String str) { return str.length() > 10;}

Вообще, если в этот метод попадёт null, то будет выброшено NullPointerException:

/* isLongString выбросит НПЕ */isLongString(null);

При этом в момент написания метода такой вариант может быть неочевиден. Но однажды написанный метод зачастую используется максимально широко во всех участках приложения, где это имеет смысл. И не всегда мы контролируем содержание данных, поступающих на обработку приложения. Данные, полученные из внешних источников практически всегда будут неполными и будут содержать null'ы. А поскольку с null'ами доступен только ограниченный набор операций, все прочие операции будут вызывать NullPointerException:

/* isLongString выбросит НПЕ, если getExternalString() вернёт нулл */isLongString(getExternalString());


Проверка параметра метода на null

Получаемые из вне (например, в качестве параметров) объекты можно и нужно проверять на равенство null'у, прежде чем приступить к их обработке. Тем более, что null нормально работает с операторами сравнения "равно" == и "не равно" !=

public boolean isLongString(String str) { if (str == null) return false; else return str.length() > 10;}


Выбрасывание NullPointerException вручную

NPE -- довольно надоедливое исключение в Java и необходимо привыкнуть к тому, что доверять данным не стоит и многие объекты приложения могут внезапно оказываться null'ами.

Таким образом, проверка объектов на равенство null'у -- довольно обычная, даже тривиальная операция. Если вы пишите публичный метод, который принимает аргументы ссылочных типов, то проверять эти аргументы на нулл -- обычное дело:

public void printObj(Object obj) { if (obj != null) { System.out.println(">>> " + obj.toString()); }}

Однако, часто мы пишем такие публичные методы, которые принципиально не могут обработать ситуацию, когда методу передан null:

public boolean isPositive(Integer i) { return i > 0;}

Если i окажется null, то про эту величину нельзя сказать, что это положительное или не положительное число. Как минимум потому, что это не число. В таких ситуациях обычно руками выбрасывается исключение:

public boolean isPositive(Integer i) { if (i == null) throw new NullPointerException("Невозможно обработать null"); return i > 0;}

Иногда в таких ситуациях также выбрасывают IllegalArgumentException.

Собственно для этих случаев Java предоставляет стандартный библиотечный метод Objects.requireNonNull:

public boolean isPositive(Integer i) { Objects.requireNonNull(i, "Невозможно обработать null"); return i > 0;}

Он сделает то же, что и строка if (i == null) throw new NullPointerException("Невозможно обработать null"); А именно, проверит, не равно ли i null'у, и если равно, то выбросит NPE с соответствующим сообщением.


Выявление места возникновения NullPointerException

До Java 14 выявление места возникновения NPE может оказаться довольно трудоёмкой задачей. Хотя трейс исключения всегда показывает строку, в которой исключение возникло:

Exception in thread "main" java.lang.NullPointerException

at Test.main(Test.java:4)

В данном выводе Test.java -- имя файла, а 4 -- номер строки, в которой возникло исключение.

Но этой информации может быть недостаточно, если у нас цепочка вызовов методов:

List<String> strings = getStringsFromSomeWhere();int length = list.get(0).length();

Если вторая строка данного примера выбросит NPE, то вообще неизвестно, что именно оказалось null'ом:

  • список, который вернул метод getStringsFromSomeWhere();

  • первый элемент этого списка (если список пуст, например).

Решается эта проблема путём создания промежуточных переменных и проверки их на равенство null'у. И чем длиннее цепочка, тем больше таких переменных приходится создавать и больше проверок делать.

К сожалению, часто данные мы получаем из методов, написанных не нами. И эти методы возвращают то, что возвращают.

Начиная с 14-й версии Java помогает точно идентифицировать, в каком именно узле цепочки вызовов произошло разыменование null'а:

in thread "main" java.lang.NullPointerException: Cannot invoke "java.util.List.size()" because "list" is null


Практики, помогающие избежать NullPointerException

Как бы то ни было, переменные и поля, инициализированные null'ами повляются в приложении и передаются из метода в метод, время от времени вызывая NullPointerException. Количество проблем можно сократить, если пользоваться следующими рекомендациями:

  • Не возвращать null'ы из методов. Если метод возвращает коллекции или массивы, то пусть в случае отсутствия конкретных данных возвращает пустые коллекции и массивы из нуля элементов.

  • Если метод возвращает объекты и есть причины, чтобы вместо конкретного объекта в тех или иных условиях вернуть null, начиная с 8-й версии Java, можно зарефакторить метод таким образом, чтобы он возвращал Optional. Работа с этим типом может казаться не всегда уместной и несколько громоздкой, но это вопрос привычки.

  • При использовании метода equals() для сравнения объектов, всегда вызывайте его на объекте, в существовании которого нет сомнений, например, new Integer(1).equals(num). Если num == null, то equals() отработает нормально и вернёт false. Главное, чтобы он был вызван на реальной ссылке.

  • Используйте String.valueOf(obj) вместо obj.toString(). Если переменной obj случится быть равной null'у, то первый вариант отработает без проблем, вернув строку "null". Второй вариант выбросит исключение.

  • Проверяйте заполненность полей классов, прежде чем их сравнивать с помощью операторов <, <=, >, >=. Конструкции получаются громоздкими, но ничего не поделаешь, сначала смотрим, что поле заполнено и только потом его сравниваем:

return m1.getNum() != null && m1.getNum > 10;
  • Помним, что анбоксинг череват проблемами с NPE в самых неожиданных местах:

public class Model { Boolean saved; public boolean isSaved() {return saved;}}

В данном примере вызов метода isSaved() на любом экземпляре класса Model чреват выбросом NPE. Поле saved по умолчанию инициализировано null'ом, а метод возвращает примитивный boolean (с маленькой буквы). Попытка привести null к boolean выбросит NPE.