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-значений. О них мы подробнее поговорим в следующей статье.