null в Java это литерал, обозначающий «ссылка никуда»: значение по умолчанию для любой ссылочной переменной, которое нельзя присвоить примитиву и у которого нельзя вызвать метод экземпляра, иначе получите NullPointerException. Ниже девять фактов о null, а затем разбор, почему возвращать null из методов плохо и чем его заменить: Нулевым Объектом, исключением или Optional.

Кратко

  • null это литерал, а не ключевое слово и не объект; это значение по умолчанию для полей ссылочных типов, а локальную переменную компилятор заставит проинициализировать явно.
  • null можно присвоить и привести к любому ссылочному типу, но не к примитиву; распаковка Integer со значением null в int дает NullPointerException, это частая ошибка с HashMap.
  • instanceof для null всегда возвращает false; статический метод через null-ссылку вызвать можно, метод экземпляра нельзя.
  • Сравнивать null можно только через == и != (null == null дает true), операторы вроде < с ним не компилируются.
  • Возвращать null из методов плохо: вызывающий обязан помнить про проверку, иначе NPE; альтернативы это Нулевой Объект, исключение (быстрое поражение) или Optional с Java 8.
  • Ленивая загрузка через поле, равное null, делает объект изменяемым и незавершенным; кэширование лучше вынести на другой уровень приложения.
Java и null неразрывно связаны. Едва ли существует Java-программист, не встречавшийся с "null pointer exception" и это печальный факт. Даже изобретатель концепции "null" назвал ее своей ошибкой на миллиард долларов, тогда зачем Java поддерживает ее? null был здесь долгое время, и я полагаю, создатели Java знают, что он создает больше проблем чем решает, так почему же они все еще мирятся с этим. И это удивляет меня еще больше, потому что философией Java было упростить вещи, вот почему они больше не возятся с указателями, перегрузкой операторов и множественным наследованием, но почему null? Ну, я действительно не знаю ответ на этот вопрос, но, что я точно знаю, не имеет значения сколько бы null критиковался Java-программистами и open-source сообществом, мы должны жить с ним. Вместо того чтобы сожалеть, лучше узнать больше и быть уверенным что мы используем null правильно.

Почему Вы должны узнать о null в Java?

Потому что, если Вы не обратите внимания на null, будьте уверены, Java заставит страдать от ужасного java.lang.NullPointerException и Вы выучите этот урок, но пойдете более трудным путем. Написание устойчивого к "падениям" кода это искусство и Ваша команда, заказчики и пользователи оценят это. По моему опыту, одна из основных причин NullPointerException это недостаток знаний о null в Java. Многие из Вас уже знакомы с null, остальные смогут узнать некоторые старые и новые вещи о ключевом слове null. Давайте повторим или узнаем некоторые важные вещи о null в Java.

Что есть null в Java

Как я говорил, null очень-очень важная концепция в Java. Первоначально он был изобретен для обозначения отсутствия чего-либо, например, отсутствие пользователя, ресурса или чего угодно, но уже в течение года озадачил Java-программистов множеством null pointer exception. В этом уроке, мы узнаем основные факты о ключевом слове null в Java, изучим некоторые приемы как избежать неприятностей с null pointer exceptions и минимизировать проверки на null.

1. null пишется только строчными буквами

Перво-наперво, null это зарезервированное слово в Java, так же как public, static или final (формально спецификация языка называет его не ключевым словом, а литералом, но использовать его как имя все равно нельзя). Регистр учитывается, Вы не можете писать null как Null или NULL, компилятор не распознает его и будет выброшена ошибка.

Object obj = NULL; // Not Ok
Object obj1 = null; //Ok
Зачастую, с этим встречаются программисты, перешедшие с других языков программирования, но при использовании современных IDE проблема становится незначительной. В наши дни, IDE вроде Eclipse или NetBeans могут исправлять эту ошибку пока Вы набираете код, но в эпоху Notepad, Vim и Emacs, это была распространенная проблема, которая могла съесть кучу драгоценного времени.

2. null это значение по умолчанию для ссылочных типов

Так же, как каждый примитив имеет значение по умолчанию, например, у int это 0, у boolean это false, null это значение по умолчанию любых ссылочных типов, проще говоря, для всех объектов. Так же, как при создании логической переменной ее значение по умолчанию равно false, так и любые ссылочные переменные в Java по умолчанию будут равны null. Это истинно для всех типов переменных: переменной-члена или локальной переменной, переменной экземпляра или статической переменной, кроме того, компилятор будет ругаться, если Вы используете локальную переменную не проинициализировав ее.

private static Object myObj;
public static void main(String args[]){
    System.out.println("What is value of myObjc : " + myObj);
}
What is value of myObjc : null
Это истинно как для статических, так и для не статических объектов, как Вы можете видеть здесь, я сделал myObj статической ссылкой, так что я могу использовать ее непосредственно в методе main, который является статическим методом и не позволяет обращаться к не статическим переменным изнутри.

3. null это не объект, но его можно привести к любому ссылочному типу

Несмотря на распространенное заблуждение, null это не объект (Object). У него есть собственный особый тип без имени (спецификация называет его null type), а само значение может быть назначено любому ссылочному типу, и Вы можете привести null к любому ссылочному типу, как показано ниже:

String str = null; // null can be assigned to String 
Integer itr = null; // you can assign null to Integer also 
Double dbl = null; // null can also be assigned to Double 
String myStr = (String) null; // null can be type cast to String 
Integer myItr = (Integer) null; // it can also be type casted to Integer 
Double myDbl = (Double) null; // yes it's possible, no error
Как Вы можете видеть, приведение null к любому ссылочному типу пройдет успешно как во время компиляции, так и во время выполнения программы. В отличие от того, что многие из Вас возможно подумали, это не приведет к выбрасыванию NullPointerException.

4. null нельзя присвоить примитиву

null может быть назначен только ссылочному типу, Вы не можете назначить null примитивной переменной вроде int, double, float или boolean. Компилятор выразит Вам свое недовольство если Вы сделаете как показано ниже:

int i = null; // type mismatch : cannot convert from null to int 
short s = null; // type mismatch : cannot convert from null to short 
byte b = null; // type mismatch : cannot convert from null to byte 
double d = null; //type mismatch : cannot convert from null to double 

Integer itr = null; // this is ok 
int j = itr; // this is also ok, but NullPointerException at runtime

Как Вы можете видеть, когда мы непосредственно присваиваем null примитиву, то получаем ошибку процесса компиляции, но, если присвоить null объекту класса-обертки, а затем присвоить этот объект соответствующему примитивному типу, компилятор не отреагирует, но мы будем вознаграждены null pointer exception во время выполнения. Это происходит из-за авто упаковки (autoboxing) в Java, и мы еще встретимся с ним в следующем пункте.

5. Распаковка null дает NullPointerException

Любой класс-обертка со значением null будет выбрасывать java.lang.NullPointerException когда Java распакует(unbox) его в примитивную переменную. Некоторые программисты делают ошибку допуская, что авто упаковка(autoboxing) позаботится о конвертации null в значение по умолчанию для соответствующего примитивного типа, например, 0 для int, false для boolean и т.д., но это не верно, в чем можно убедиться ниже:

Integer iAmNull = null; 
int i = iAmNull; // Remember - No Compilation Error
Но, когда Вы запустите данный фрагмент кода, в консоли Вы увидите

Exception in thread "main" java.lang.NullPointerException
Это часто происходит при работе с HashMap и Integer key. Выполнение кода, показанного ниже прервется, как только Вы его запустите.

import java.util.HashMap; 
import java.util.Map; 
/** 
* An example of Autoboxing and NullPointerExcpetion
* 
* @author WINDOWS 8 
*/ 
public class Test { 
    public static void main(String args[]) throws InterruptedException { 
        Map<Integer, Integer> numberAndCount = new HashMap<>(); 
        int[] numbers = {3, 5, 7,9, 11, 13, 17, 19, 2, 3, 5, 33, 12, 5}; 
        for(int i : numbers){ 
            int count = numberAndCount.get(i); 
            numberAndCount.put(i, count++); // NullPointerException here 
        } 
    } 
}


Output: Exception in thread "main" java.lang.NullPointerException at Test.main(Test.java:25)
Этот код выглядит очень простым и безобидным. Вы просто подсчитываете сколько раз число встречается в массиве, классическая техника нахождения дубликатов. Разработчик берет предыдущее подсчитанное количество, увеличивает его на единицу и вставляет обратно в Map. Он мог бы подумать, что авто-упаковка позаботится о преобразовании Integer в int, как это делается в момент вызова метода put(), но он забывает, что если для числа подсчет еще не проводился, метод get() вернет из HashMap null, не ноль, потому что значение по умолчанию для Integer это null, а не 0, и авто-упаковка выбросит null pointer exception при попытке сконвертировать Integer в переменную int.

6. instanceof для null возвращает false

Оператор instanceof будет возвращать false если в качестве параметра указать любую ссылочную переменную со значением null или null сам по себе. Пример:

Integer iAmNull = null; 
if(iAmNull instanceof Integer){ 
    System.out.println("iAmNull is instance of Integer"); 
}else{ 
    System.out.println("iAmNull is NOT an instance of Integer"); 
}


Output : iAmNull is NOT an instance of Integer
Это важное свойство оператора instanceof, которое делает его полезным для проверки приведения типов.

7. Статический метод можно вызвать через null-ссылку

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

public class Testing { 
    public static void main(String args[]){ 
        Testing myObject = null; 
        myObject.iAmStaticMethod(); 
        myObject.iAmNonStaticMethod(); 
    } 
    private static void iAmStaticMethod(){ 
        System.out.println("I am static method, can be called by null reference"); 
    } 
    private void iAmNonStaticMethod(){ 
        System.out.println("I am NON static method, don't dare to call me by null"); 
    } 
}


Output: I am static method, can be called by null reference 
Exception in thread "main" java.lang.NullPointerException 
at Testing.main(Testing.java:11)

8. null можно передать как аргумент метода

Вы можете послать null в качестве параметра метода, принимающего любой ссылочный тип, к примеру:

 public void print(Object obj)
может быть вызван как

print(null)
Это нормально с точки зрения компилятора, но дальнейшее поведение полностью зависит от метода. Null-безопасный метод не выбросит NullPointerException, а просто корректно завершится. Если бизнес-логика позволяет, рекомендуется писать null-безопасные методы.

9. null сравнивают только через == и !=

Вы можете сравнивать null используя операторы == (равно) и != (не равно), но не можете использовать его с другими арифметическими или логическими операторами, вроде < (меньше) или > (больше). В отличие от SQL, в Java null == null вернет true, как показано ниже:

public class Test {
	public static void main(String args[]) throws InterruptedException {
		String abc = null; 
		String cde = null; 
		if(abc == cde){ 
			System.out.println("null == null is true in Java"); 
		} 
		if(null != null){ 
			System.out.println("null != null is false in Java"); 
		} 
		// classical null check 
		if(abc == null){ 
			// do something 
		} 
		// not ok, compile time error 
		if(abc > null){ 
		
		} 
	} 
}


Output: 
null == null is true in Java

Почему NULL это плохо?

Посмотрите на такой метод:
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return null;
  }
  return new Employee(id);
}
Что не так с этим методом? Он может вернуть NULL вместо объекта, вот что не так. Использование NULL это ужасная практика в ООП, и этого стоит избегать всеми способами. По данному вопросу уже опубликовано достаточно разных мнений, в том числе презентация Tony Hoare «Нулевые ссылки: Ошибка на миллиард долларов» и целая книга David West «Объектно-ориентированное мышление». Здесь я попытаюсь суммировать все доводы и показать примеры того, как можно избежать использования NULL, заменив его подходящими объектно-ориентированными конструкциями. Сначала рассмотрим две возможные альтернативы NULL. Первая это паттерн проектирования «Нулевой Объект» (лучше всего реализовывать его с помощью константы):
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Employee.NOBODY;
  }
  return new Employee(id);
}
Вторая возможная альтернатива это «быстрое поражение» через выбрасывание исключения в случае, если вернуть объект невозможно:
public Employee getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    throw new EmployeeNotFoundException(name);
  }
  return new Employee(id);
}

Третья альтернатива: Optional (с Java 8)

Оригинал этого эссе написан до Java 8, а с нее в языке есть третий способ честно сказать «результата может не быть»: java.util.Optional. Метод возвращает контейнер, который либо содержит объект, либо пуст, и вызывающий уже не может забыть про проверку, потому что до объекта иначе не добраться:
public Optional<Employee> getByName(String name) {
  int id = database.find(name);
  if (id == 0) {
    return Optional.empty();
  }
  return Optional.of(new Employee(id));
}
На стороне вызывающего решение принимается явно: orElseThrow превращает пустой результат в исключение, orElse подставляет объект по умолчанию (тот самый Нулевой Объект), ifPresent выполняет действие только при наличии значения:
dept.getByName("Jeffrey")
  .orElseThrow(() -> new EmployeeNotFoundException("Jeffrey"))
  .transferTo(dept2);
Две оговорки из документации Oracle. Во-первых, Optional задуман именно для возвращаемых значений методов; для полей классов и параметров методов он не предназначен, там по-прежнему работают Нулевой Объект и проверки на входе. Во-вторых, переменная типа Optional сама никогда не должна быть null: вернуть null вместо Optional.empty() значит обесценить всю идею. А теперь давайте познакомимся с доводами против использования NULL

Обработка ошибок вручную

Каждый раз, когда вы на входе получаете объект, вы должны проверять, является он ссылкой на действительный объект или NULL’ом. Если вы забудете проверить, ваша программа может быть прервана прямо во время исполнения выброшенным NullPointerException (NPE). Из-за этого ваш код начинает наполняться многочисленными проверками и разветвлениями if/then/else.
// this is a terrible design, don't reuse
Employee employee = dept.getByName("Jeffrey");
if (employee == null) {
  System.out.println("can't find an employee");
  System.exit(-1);
} else {
  employee.transferTo(dept2);
}
Именно так исключительные ситуации должны обрабатываться в С и других строго процедурных языках программирования. В ООП обработка исключений введена главным образом как раз для того, чтобы избавиться от вручную написанных блоков обработки. В ООП мы позволяем исключениям всплывать пока они не достигнут обработчика ошибок всего приложения, и благодаря этому наш код становится намного чище и короче:
dept.getByName("Jeffrey").transferTo(dept2);
Считайте NULL ссылки пережитками процедурного стиля программирования и используйте 1) Нулевые Объекты или 2) Исключения вместо них.

Неоднозначное понимание

Чтобы точно передать в названии смысл происходящего, метод getByName() должен быть переименован в getByNameOrNullIfNotFound(). То же самое нужно сделать для каждого метода, который возвращает объект или NULL, иначе при чтении кода не избежать неоднозначности. Таким образом, для того, чтобы названия методов были точны, вы должны давать методам более длинные имена. Чтобы избежать неоднозначности всегда возвращайте реальный объект, нулевой объект или выбрасывайте исключение. Кто-то может возразить, что иногда нам просто необходимо возвратить NULL чтобы добиться нужного результата. Например, метод get() интерфейса Map в Java возвращает NULL, когда в Map нет объекта с таким ключом.
Employee employee = employees.get("Jeffrey");
if (employee == null) {
  throw new EmployeeNotFoundException();
}
return employee;
Благодаря использованию NULL в Map этому коду хватает всего одного цикла поиска для получения результата. Если мы перепишем Map таким образом, чтобы метод get() выбрасывал исключение в случае, если ничего не найдено, наш код будет выглядеть так:
if (!employees.containsKey("Jeffrey")) { // first search
  throw new EmployeeNotFoundException();
}
return employees.get("Jeffrey"); // second search
Очевидно, что этот метод в два раза медленнее, чем исходный. Что же делать? В интерфейсе Map (без намерения обидеть разработчиков) есть недостаток проектирования. Его метод get() должен был бы возвращать Iterator, и тогда наш код выглядел бы так:
Iterator found = Map.search("Jeffrey");
if (!found.hasNext()) {
  throw new EmployeeNotFoundException();
}
return found.next();
Кстати, именно так спроектирован метод STL map::find() в С++.

Компьютерное мышление против объектно-ориентированного

Строка кода if (employee == null) вполне понятна тому, кто знает, что объект в Java это указатель на структуру данных, а NULL это указатель на ничто (в процессорах Intel x86 это 0x00000000). Однако если вы начнете мыслить в объектном стиле, эта строка становится намного менее осмысленной. Вот как наш код выглядит с объектной точки зрения:
- Здравствуйте, это отдел разработки ПО? - Да. - Будьте добры, пригласите к телефону вашего сотрудника Джефри. - Подождите минутку... - Здравствуйте. - Вы NULL?
Последний вопрос звучит немного странно, не так ли? Если вместо этого после вашей просьбы пригласить к телефону Джефри на том конце просто повесят трубку, это вызовет для нас определенные сложности (Исключение). В этом случае мы можем попробовать перезвонить или же доложим нашему начальнику о том, что мы не смогли поговорить с Джефри, и завершим свою основную задачу. Кроме этого, на той стороне вам могут предложить поговорить с другим человеком, который, хоть и не является Джефри, может либо помочь вам с большинством ваших вопросов, либо отказаться помогать, если нам нужно узнать что-то, что знает только Джефри (Нулевой Объект).

Медленный провал

Вместо быстрого завершения работы, код выше пытается умереть медленно, убивая других на своем пути. Вместо того, чтобы дать всем понять, что что-то пошло не так и нужно немедленно начинать обработку исключительного события, он пытается скрыть свой провал от клиента. Это очень похоже на ручную обработку исключений, о которой мы говорили выше. Делать свой код как можно более хрупким и позволять ему прерываться, если это нужно, это хорошая практика. Делайте свои методы предельно требовательными к данным, с которыми они работают. Позволяйте им жаловаться, выкидывая исключения, если данных, которые им предоставили, недостаточно, или же данные просто не подходят для использования в этом методе по задуманному сценарию. В противном случае возвращайте Нулевой Объект, который ведет себя каким-то общепринятым способом и выбрасывает исключения во всех других случаях.
public Employee getByName(String name) {
  int id = database.find(name);
  Employee employee;
  if (id == 0) {
    employee = new Employee() {
      @Override
      public String name() {
        return "anonymous";
      }
      @Override
      public void transferTo(Department dept) {
        throw new AnonymousEmployeeException(
          "I can't be transferred, I'm anonymous"
        );
      }
    };
  } else {
    employee = new Employee(id);
  }
  return employee;
}

Изменяемые и незавершенные объекты

Вообще, строго рекомендуется проектировать объекты так, чтобы они были неизменяемыми. Это значит, объект должен получить все необходимые данные при его создании и никогда не менять своего состояния в течение всего жизненного цикла. Значения NULL очень часто используются в паттерне проектирования «Ленивая загрузка» для того, чтобы сделать объекты незавершенными и изменяемыми. Пример:
public class Department {
  private Employee found = null;
  public synchronized Employee manager() {
    if (this.found == null) {
      this.found = new Employee("Jeffrey");
    }
    return this.found;
  }
}
Несмотря на то, что эта технология широко распространена, для ООП она является антипаттерном. И главным образом потому, что заставляет объект нести ответственность за проблемы с производительностью у вычислительной платформы, а это как раз то, о чем объект Employee не может быть осведомлен. Вместо того, чтобы управлять своим состоянием и вести себя соответствующим своему предназначению образом, объект вынужден заботиться о кэшировании своих собственных результатов, вот к чему приводит «ленивая загрузка». А ведь кэширование это вовсе не то, чем занимается сотрудник в офисе, не так ли? Выход? Не используйте «ленивую загрузку» таким примитивным способом, как в вышеприведенном примере. Вместо этого переместите кэширование проблем на другой уровень своего приложения. Например, в Java вы можете использовать возможности аспектно-ориентированного программирования. Например, в jcabi-aspects есть аннотация @Cacheable, которая кэширует значение, возвращаемое методом.
import com.jcabi.aspects.Cacheable;
public class Department {
  @Cacheable(forever = true)
  public Employee manager() {
    return new Employee("Jacky Brown");
  }
}

Что возвращать вместо null: сводка

Сведем варианты для метода, который может ничего не найти:
Подход Что получает вызывающий Когда уместен Минусы
вернуть null ссылку, которую обязан проверить сам внутри одного класса, где проверка рядом и очевидна забытая проверка это NullPointerException в случайном месте; по имени метода не понятно, что он может вернуть null
Нулевой Объект объект-заглушку с безопасным поведением по умолчанию когда «ничего» это нормальный вариант: анонимный пользователь, пустой список слушателей заглушка может скрыть ошибку, если ее поведение неочевидно
исключение (быстрое поражение) ничего: выполнение прерывается сразу когда отсутствие объекта это ошибка, а не штатная ситуация исключения ради обычного потока управления делают код тяжелым
Optional (с Java 8) контейнер, который явно говорит «значения может не быть» для возвращаемых значений методов, где «не найдено» это штатный исход не для полей и параметров; сам Optional тоже можно по ошибке вернуть как null
Надеюсь, этот анализ был достаточно убедителен, чтобы вы прекратили обNULLять свой код :) Это все о null в Java. Наработав некоторый опыт в Java-программировании и используя простые уловки, чтобы избежать NullPointerException, Вы сможете сделать Ваш код безопасным. Т.к. значение null может рассматриваться как пустое или неинициализированное значение, это часто является источником путаницы, вот почему так важно документировать поведение метода при входящем значении null. Всегда помните, null это значение по умолчанию ссылочных переменных, и Вы не можете вызывать методы экземпляра или получать доступ к переменным экземпляра используя null-ссылку в Java.

Вопросы и ответы

Что такое null в Java простыми словами?

Это специальное значение «ссылка никуда». По спецификации языка null это литерал (не ключевое слово и не объект), у которого есть собственный безымянный тип; его можно присвоить любой ссылочной переменной, но не примитиву. Поля ссылочных типов получают null автоматически, если их не инициализировать. Вызов метода экземпляра или обращение к полю через такую ссылку заканчивается NullPointerException.

Как понять, что именно было null при NullPointerException?

Читать сообщение исключения. С Java 14 (JEP 358) JVM формирует подробные сообщения вида «Cannot invoke "String.length()" because "name" is null»: в них названы и метод, и переменная или поле, которые оказались null; с Java 15 это включено по умолчанию. В старых версиях остается только номер строки из стектрейса, и если в строке несколько вызовов через точку, виновника приходится искать отладчиком.

Как правильно проверять на null?

Через == и !=: if (obj == null). Метод equals для этого не годится: у null его нельзя вызвать, зато безопасно писать "admin".equals(role), где слева заведомо не null. Для параметров есть Objects.requireNonNull(obj, "сообщение"): он бросает NullPointerException сразу на входе в метод, с понятным текстом, а не где-то глубже. В лямбдах удобны Objects::isNull и Objects::nonNull.

Что такое Optional и заменяет ли он null?

Это контейнер из Java 8, который либо содержит значение, либо пуст, и заставляет вызывающего явно решить, что делать во втором случае: orElse, orElseThrow, map, ifPresent. Документация Oracle прямо говорит, что Optional задуман для возвращаемых значений методов, где нужно выразить «результата нет», и что переменная типа Optional сама никогда не должна быть null. Для полей классов и параметров методов он не предназначен, так что полностью null он не заменяет, но убирает его из сигнатур.

Можно ли положить null в коллекцию?

Зависит от коллекции. ArrayList, LinkedList и HashMap принимают null (в HashMap допустим один null-ключ и сколько угодно null-значений). TreeMap с естественным порядком null-ключ не примет: ему нечего сравнивать. Неизменяемые коллекции List.of, Set.of, Map.of и ConcurrentHashMap бросают NullPointerException при попытке положить null. Поэтому проверка «содержит ли Map ключ» через get() != null ненадежна, для нее есть containsKey.

Чем Нулевой Объект отличается от исключения и что выбрать?

Нулевой Объект это заглушка того же типа, которая ведет себя безопасно: анонимный сотрудник с именем «anonymous», пустой список. Он уместен, когда «ничего» это нормальный вариант работы, и код может продолжаться. Исключение уместно, когда отсутствие объекта это ошибка: программа должна остановиться сразу, а не тащить некорректное состояние дальше (принцип быстрого поражения). Общее правило из статьи: возвращайте реальный объект, Нулевой Объект или бросайте исключение, но не null.

Читайте также