Null-безопасность в Java: когда нули тоже имеют значение

В компании «Свой Банк» мы активно развиваем лучшие практики и стандарты в Backend-разработке. Но, прежде чем выработать хотя бы одну практику, необходимо изучить материалы, разобраться в теме и выработать подходящий вариант. Поэтому в данной статье затронем основные понятия и концепции работы null-безопасности в объектно-ориентированном языке программирования Java.
Что такое Null-безопасность и Nullability
Nullability — это концепция, которая описывает, может ли переменная или выражение содержать значение null. Несмотря на свою кажущуюся простоту, работа с null часто становится причиной сложных ошибок, таких как NullPointerException (NPE). Из-за этого возникла концепция null-безопасности, целью которой является защита кода от ошибок, связанных с null.
Null-безопасность означает, что в коде либо отсутствуют null-значения, либо они обрабатываются явно и безопасно. В Java существуют разные подходы к обеспечению null-безопасности, которые позволяют управлять возможностью появления null в коде и минимизировать риски, связанные с их использованием.
Итого Nullability — это способность объекта быть null.
Null-безопасность — это свойство языка программирования безопасно работать с null.
Null-безопасность в Java
Java изначально не имела встроенных механизмов для безопасной работы с null-значениями, что привело к распространению ошибок, вызванных его использованием.
Для предотвращения NPE требовалась проверка объектов перед их использованием, либо приходилось обрабатывать NullPointerException в try catch конструкции.
Начиная с версии Java 5, были добавлены аннотации, которые позволили добавлять метаданные о намерении использовать null-значения. Они помогают разработчикам и инструментам статического анализа понять, какие переменные могут быть null, а какие нет.
В версии Java 8 был добавлен Null wrapper Optional, который предоставляет способ работы с потенциально null значениями без необходимости явных проверок.
Пример Optional:
Optional optionalName = Optional.ofNullable(user.getName());
optionalName.ifPresent(name -> System.out.println(name))
Однако даже с этими инструментами null-безопасность в Java требует внимания и дисциплины, так как многие старые библиотеки и API активно используют null.
Что такое null?
В Java null означает отсутствие объекта. Он используется для обозначения неинициализированных объектов, отсутствующих данных, а также для обозначения ситуаций, когда переменной присваивается неизвестное или неопределенное значение.
В отличие от других объектов, null не содержит никаких данных и не поддерживает методы. Попытка вызвать метод на null приводит к NullPointerException.
Пример использования null:
String name = null
Здесь переменная name не ссылается на какой-либо объект. Если попытаться вызвать метод этой переменной, программа выбросит исключение.
Когда возникает NullPointerException?
NullPointerException (NPE) — это одна из самых распространенных ошибок в Java. Она возникает, когда программа пытается использовать объект, который равен null. Это может привести к неожиданным сбоям и сложностям в отладке. Проблемы начинаются, когда код пытается работать с null так же, как с обычным объектом.
Пример возникновения NullPointerException:
User user = null;
String name = user.getName(); // Возникает NullPointerException (NPE)
Значение переменной user равно null, и при попытке вызвать метод getName() происходит сбой.

Как используется null?
В зависимости от написанного кода каждому объекту можно дать характеристику на время его жизни:
Всегда
null.
Например, поле в объекте, которое никогда не инициализируется и не заполняется значением. В «Своем Банк» мы стремимся не создавать такие поля, даже если оно планируется использоваться когда-то в будущем, следуем принципу YAGNI («You aren’t gonna need it»).
Всегда не
null.
Например, поле в объекте, которое всегда инициализируется значением.
Nullable до определенного этапа жизни.
Например, поле в объекте, которое инициализируется как null, но на определенном этапе всегда заполняется не null значением.
Nullable в любой момент времени.
Например, поле в объекте, которое в любой момент времени заполняется либо null-значением, либо не null-значением.
Nullable объекты является самой частой причиной NullPointerException, т.к. не дают явного понимания, когда с объектом работать безопасно.
Рассмотрим варианты, когда используются null:
1. Неинициализированное значение
Объект объявлен, но не инициализирован. По умолчанию для объектов переменным присваивается null.
Пример:
Object obj = null;
2. Отсутствующее значение или возвращение null из методов
При вызове метода, который возвращает значение, иногда можно получить null, если результат отсутствует, как в случае с методом Map.get().
Пример:
Map map = new HashMap<>();
String value = map.get("key"); // Может вернуть null, если ключ не найден
3. Незаполненное значение из внешних систем:
Пустые поля из внешних систем могут быть представлены как null.
Пример:
String json = "{"name": "Ivan", "age": null}";
User user = objectMapper.readValue(json, User.class);
user.getAge(); // вернет null
4. Неизвестное значение
Например, неизвестное или неопределённое значение из перечисления (enum).
Итого null — это контракт и намерение.

Избавиться от NullPointerException — это не использовать null.
В Java полностью избежать использования null невозможно из-за обратной совместимости и распространенного использования этого значения в стандартных библиотеках и фреймворках.
Разработчики в «Своем Банке» стремятся не возвращать null «by design». Проверки на null все же необходимы там, где потенциально может возникнуть ситуация с его использованием.
Многие языки имеют null-значения, но главное отличие между ними — это возможность избегать использование null.
Мы уже поняли, что в Java невозможно избегать null. Другие языки имеют иные подходы для работы с null, например, в Kotlin встроена возможность избегать использование null, но даже это не исключает ошибок разработке и возникновения NullPointerException при использовании Java библиотек или интеграции с другими системами.
Несмотря на то что null является неотъемлемой частью Java, существуют инструменты и библиотеки, которые помогают минимизировать его использование.

Решения
1. Non-null по умолчанию — объекты не могут быть null, если это явно не указано.
В Java невозможно получить non-null по умолчанию, но можно добиться гарантии non-null:
Проверками на
nullво время создания объекта;Использованием аннотаций
@Nonnull,@ParametersAreNonnullByDefaultи статических анализаторов
2. Null wrapper — обертка для объектов, которые могут быть null.
Вместо возвращения null можно возвращать объект класса Optional, который помогает избежать прямой работы с null.
Это заставляет явно обрабатывать случай отсутствия значения.
Пример Null wrapper:
Optional optionalName = Optional.ofNullable(..);
optionalName.ifPresent(name -> System.out.println(name.length()));
3. Явные проверки
Самый простой и распространенный способ избежать NullPointerException — это проверка значения на null перед его использованием.
Пример:
if (name != null) {
int length = name.length();
}
Заключение
Null-безопасность — это подход, который помогает сделать работу с null более явной, понятной и безопасной. Чтобы добиться null-безопасности, можно использовать аннотации, Optional и другие подходы, предотвращающие передачу и возврат null-значений. О них мы подробнее поговорим в следующей статье.
