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

01bf3077dff5d03f75e8e94266e7b77d.png

В компании «Свой Банк» мы активно развиваем лучшие практики и стандарты в 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() происходит сбой.

9c433f0fd0fe894112e6c91260fa57fa.png

Как используется null?

В зависимости от написанного кода каждому объекту можно дать характеристику на время его жизни:

  1. Всегда null.

Например, поле в объекте, которое никогда не инициализируется и не заполняется значением. В «Своем Банк» мы стремимся не создавать такие поля, даже если оно планируется использоваться когда-то в будущем, следуем принципу YAGNI («You aren’t gonna need it»).

  1. Всегда не null.

Например, поле в объекте, которое всегда инициализируется значением.

  1. Nullable до определенного этапа жизни.

Например, поле в объекте, которое инициализируется как null, но на определенном этапе всегда заполняется не null значением.

  1. 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 — это контракт и намерение.

acf28fef7ab062342fe9a36c983b7b7a.jpeg

Избавиться от NullPointerException — это не использовать null.

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

Разработчики в «Своем Банке» стремятся не возвращать null «by design». Проверки на null все же необходимы там, где потенциально может возникнуть ситуация с его использованием.

Многие языки имеют null-значения, но главное отличие между ними — это возможность избегать использование null.

Мы уже поняли, что в Java невозможно избегать null. Другие языки имеют иные подходы для работы с null, например, в Kotlin встроена возможность избегать использование null, но даже это не исключает ошибок разработке и возникновения NullPointerException при использовании Java библиотек или интеграции с другими системами.

Несмотря на то что null является неотъемлемой частью Java, существуют инструменты и библиотеки, которые помогают минимизировать его использование.

95646275c5a7de6975f86ced3bd3e44c.jpeg

Решения

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

© Habrahabr.ru