В компании “Свой Банк” мы активно развиваем лучшие практики и стандарты в 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<String> 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<String, String> 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<String> 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-значений. О них мы подробнее поговорим в следующей статье.
Комментарии (15)
rsashka
15.10.2024 07:50Вы боретесь не причиной, а со следствием (NullPointerException), тогда как причина возникновения этого исключения - нарушение логики работы приложения, но с ней вы так ничего и не делаете при явных проверках. Ведь в последнем случае правильнее был бы примерно вот такой код:
if (name != null) { int length = name.length(); } else { throw new NameEmptyException(); }
То есть от исключения вы все равно никуда деваетесь.
Но если вы обрабатываете ошибки логики с помощью кодов возврата, вот тогда любые исключения будут мешаться. Но в этом случае проблема опять же не в самих NullPointerException, а в том, что у вас перемешиваются способы обработки ошибок (с помощью исключений и кодов возврата). И тогда да, первые два способа Non-null по умолчанию и Null wrapper частично закрывают данную проблему, но к сожалению не избавляют от исключений полностью. Поэтому даже в этих случаях все равно приходится ловить исключения, хоть и в меньшем объеме, чем с nullable объектами.
WelcomeToRussia Автор
15.10.2024 07:50Все зависит от реализуемой логики. Явные проверки необходимы, если объект nullable и это ожидаемое поведение. Вы привели только лишь один случай, когда null - это ошибка:
Если
name
не должен бытьnull
и это ошибка, то несомнено нужно бросить исключение, но читаемое и понятное.
if (name != null) { return name.length(); } else { throw new ViolationException('Expected non-null value on field [name].'); }
А вот несколько примеров, когда
null
- это ожидаемое значениеprivate static int DEFAULT_LENGTH = 10; .. if (name != null) { return name.length(); } else { return DEFAULT_LENGTH; }
if (name != null) { return Optional.of(name.length()); } else { return Optional.empty(); }
m_chrom
15.10.2024 07:50Разработчики в “Своем Банке” стремятся не возвращать null "by design"
А что делать если null - валидное значение. Так-то null вполне естественный способ вернуть ответ "того, что ты ищешь, у меня нет"
borman712
15.10.2024 07:50Optional же. Запросил, получил Optional, дальше - по обстоятельствам. В целом Optional спасает и от NPE в цеплочке вида country.getCity().getDistrict().getStreet().getHouse().getApartment() благодаря методу map.
Но всё равно не идеально... Думаю, если бы была идеальная серебряная пуля от NPE, её бы уже все использовали и не задавали вопросов.
m_chrom
15.10.2024 07:50На мой вкус Optional ужасно раздувает и делает плохочитаемым код. А уж если у вас больше одной nullable переменной, то замена строки
if (a != null && b != null) {}
на альтернативу сOptional
будет выглядеть как небольшая программа на ЛиспеМой вопрос не к тому, что нужно искать какую-то другую "серебряную пулю". Я скорее к тому, что не стоит так уж демонизировать null. Вполне нормальная практика его возвращать, когда это уместно, как по мне. Аннотации
@Nullable/@NotNull
и ворнинги в IDE решают почти все потенциальные проблемы с использованием null.Optional спасает и от NPE в цеплочке вида country.getCity().getDistrict().getStreet().getHouse().getApartment()
В такого рода вызовах Optional - то, что надо, да.
WelcomeToRussia Автор
15.10.2024 07:50Согласен.
Есть еще способ - это default или null object.
Пример реализации null object можно подглядеть в jackson: https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/node/NullNode.html
WelcomeToRussia Автор
15.10.2024 07:50Поле в объекте может быть null и это может быть валидное значение.
Но при этом getter поля может возвратить Optional или Null\Default object.
Например,
class .. { private String name; public Optional<String> getName() { return Optional.ofNullable(name); } }
или
class .. { private static final String DEFAULT_NAME = 'UNKNOWN'; private String name; public String getName() { return name == null ? DEFAULT_NAME : name; } }
Не вспомню случая, когда возвратить из метода
null
значение - это было бы хорошо. Разве что для обратной совместимости или при использовании библиотек, где возвращениеnull
- это часть контрактаm_chrom
15.10.2024 07:50А чем Null-Object удобнее в обработке, чем простой null? Если каждый раз писать проверку вида
if (value != NULL_VALUE) ...
то разницы вроде и никакой. Только этот NULL_VALUE надо еще для каждого класса задефайнить и где-то в глобальной переменной держать. А null из коробки есть. И инспекции в IDE опять же с null подскажут, что я проверку упустил, а с NULL_VALUE - уже нет.
А ежели проверки не писать, то это будет источником багов, которые отлавливаются куда хуже, чем NPE. NPE хотя бы fail fast.
Optional да, лучше. Но выше уже отвечал, что он уж больно код раздувает. Хотя это вкусовщина, возможно
WelcomeToRussia Автор
15.10.2024 07:50Null-object не нужно проверять на то, что он "пустой". С ним работают так же как с обычным классом, но у него поведение "пустоты".
Только этот NULL_VALUE надо еще для каждого класса задефайнить и где-то в глобальной переменной держать.
Конечно для каждого класса нужно описать Null-object, но это часть логики. Не во всех задачах подойдет такое решение.
А null из коробки есть.
null - есть и коробки, но его появление, как правило становиться неожиданностью, если не использовать аннотации или Optional.
А чем Null-Object удобнее в обработке, чем простой null? Если каждый раз писать проверку вида
Далее приведу простые примеры реализации null-object, которые дадут понять, что не нужно писать проверки:
строка содержащая что то:
String someString = "I'm not empty";
И пустая строка, которая по сути null-object:
String nullObjectString = "";
И с
someString
иnullObjectString
работать можно не думая о проверке наnull
.Более сложный пример с пользовательским классом:
interface Friend { void sendEmail(); String getName(); } class BestFriend extends Friend { private final String name = "m_chrom"; public void sendEmail() { // send email } public String getName() { return name; } } class NullFriend extends Friend { private static final String name = 'Unknown'; public void sendEmail() { //do nothing } String getName() { return name; } }
m_chrom
15.10.2024 07:50"более сложный" пример как раз хорошо объясняет то, что я имел в виду. Если не использовать проверки, то в каком-то интерфейсе вашего продукта у вас всплывет фейковый друг с именем "Unknown" и кнопкой "Отправить письмо", которая ничего не делает. Хотя логичнее было бы увидеть сообщение "Друг не задан, выберите друга".
Я не говорю, что Null-object антипаттерн. Он полезен в специфических случаях. Но как универсальную замену null-ов я бы его не советовал. Правило "всегда использовать использовать аннотации
@Nullable
там, где может прилететь null" выглядит проще и безопаснееWelcomeToRussia Автор
15.10.2024 07:50то в каком-то интерфейсе вашего продукта у вас всплывет фейковый друг с именем "Unknown" и кнопкой "Отправить письмо", которая ничего не делает.
Это вопрос требований и реализации логики. Где то уместно вывести имя "Unknown" и ничего не делать при "Отправить письмо" , а где то - нет.
Правило "всегда использовать использовать аннотации
@Nullable
там, где может прилететь null"Есть разные инструменты работы с nullable объектами, каждый инструмент подходит под свою задачу. О них подробнее напишу в следующей статье.
Nullable
подойдет, чтобы пометить поле в объекте.Optional
и null-object подойдут для того, чтобы возвратить значение из метода.Nullable
работает только в compile time.Optional
и null-object работают в runtime.m_chrom
15.10.2024 07:50Это вопрос требований и реализации логики. Где то уместно вывести имя "Unknown" и ничего не делать при "Отправить письмо" , а где то - нет.
Со всем уважением, но я с трудом представляю требование показывать пользователю технический объект-заглушку там, где на самом деле объекта нет.
Nullable
подойдет, чтобы пометить поле в объекте.А чем плохо помечать
Nullable
метод?Nullable
работает только в compile time.Так если мы говорим о способе избегать NPE, то есть по сути о борьбе с человеческими ошибками, то compile time вполне достаточно. Ну
Optional
будет более строгим, да. Но ценой читабельности кода.Еще раз уточню с чем я спорю. Я согласен с посылом, что можно использовать
Optional
для возвращения потенциальноnull
значений (если не пугает объем кода). Я также не вижу проблемы в использовании Null-object для определенных кейсов. Мне показалось излишне строгим предложение никогда не возвращатьnull
из методов. Потому что я не вижу в этом ничего плохого, если при этом не забывать помечать метод соответствующей аннотацией.WelcomeToRussia Автор
15.10.2024 07:50Мне показалось излишне строгим предложение никогда не возвращать
null
из методов.Допустим нам необходимо получить книгу
getBook(String id)
и получить количество страницbook.getTotalPages()
.Рассмотрим 3 случая
Nullable
//Реализация метода: @Nullable Book getBook(String id) { Book book = // ищем где то книгу, если не найдем вернется null return book; } //Реализация получения количества страниц, //которая может привести к NullPointerException: totalPages = getBook("Clean code").getTotalPages() //Если обратить внимание на подсветку от среды разработки // или на алерты от статического анализа // или заглянуть в описание метода, // то пишем безопасный код: int totalPages = 0; Book book = getBook("Clean code"); if (book != null) { totalPages = book.getTotalPages() }
Optional
//Реализация метода: Optional<Book> getBook(String id) { Book book = // ищем где то книгу, если не найдем вернется null return Optional.ofNullable(book); } //Нативная реализация получения количества страниц, //которая вынуждает работать безопасно: totalPages = getBook("Clean code") .map(book -> book.getTotalPages()) .orElse(0)
Null-object
class Book { public static Book emptyBook() { return //создаем пустую книгу, без страниц // getTotalPages() будет возвращать 0 } } //Реализация метода: @Nonnull Book getBook(String id) { Book book = // ищем где то книгу, если не найдем вернется null if (book == null) { return Book.emptyBook(); } else { return book; } } //Реализация получения количества страниц, //которая работает безопасно: totalPages = getBook("Clean code");
Итого при реализации
getBook
с
Nullable
мы даем возможность ошибиться разработчику при его использованиис
Optional
мы обязываем обработать отсутствие значениес
Null-object
мы снимаем с разработчика дополнительную логику и реализовываем самиНе нужно вам возвращать
null
из метода, уж поверьте. Не мне так популярным фреймворкам, где это применяют.
AdrianoVisoccini
На этой фразе у Годзиллы случился сердечный приступ и он умер