«1. Обзор
Как правило, с нулевыми переменными, ссылками и коллекциями сложно работать в Java-коде. Их не только трудно идентифицировать, но и сложно с ними справляться.
На самом деле любой промах при работе с нулевым значением не может быть идентифицирован во время компиляции и приводит к исключению NullPointerException во время выполнения.
В этом уроке мы рассмотрим необходимость проверки на null в Java и различные альтернативы, которые помогут нам избежать проверки на null в нашем коде.
2. Что такое NullPointerException?
Согласно Javadoc для исключения NullPointerException, оно генерируется, когда приложение пытается использовать null в случае, когда требуется объект, например:
-
Вызов метода экземпляра нулевого объекта. Доступ или изменение поля нулевого объекта. объект Принимает длину null, как если бы это был массив. Доступ или изменение слотов null, как если бы это был массив. исключение:
Здесь мы попытались вызвать вызов метода для нулевой ссылки. Это приведет к исключению NullPointerException.
public void doSomething() {
String result = doSomethingElse();
if (result.equalsIgnoreCase("Success"))
// success
}
}
private String doSomethingElse() {
return null;
}
Другим распространенным примером является попытка доступа к нулевому массиву:
Это вызывает NullPointerException в строке 6.
public static void main(String[] args) {
findMax(null);
}
private static void findMax(int[] arr) {
int max = arr[0];
//check other elements in loop
}
Таким образом, доступ к любому полю, методу или индексу нулевого объекта вызывает NullPointerException , как видно из приведенных выше примеров.
Обычный способ избежать исключения NullPointerException — проверить значение null:
В реальном мире программистам трудно определить, какие объекты могут иметь значение null. Агрессивно безопасной стратегией может быть проверка нуля для каждого объекта. Однако это приводит к большому количеству избыточных проверок null и делает наш код менее читаемым.
public void doSomething() {
String result = doSomethingElse();
if (result != null && result.equalsIgnoreCase("Success")) {
// success
}
else
// failure
}
private String doSomethingElse() {
return null;
}
В следующих нескольких разделах мы рассмотрим некоторые альтернативы Java, позволяющие избежать такой избыточности.
3. Обработка null с помощью контракта API
Как обсуждалось в предыдущем разделе, доступ к методам или переменным нулевых объектов вызывает исключение NullPointerException. Мы также обсудили, что проверка объекта на null перед доступом к нему устраняет возможность исключения NullPointerException.
Однако часто существуют API, которые могут обрабатывать нулевые значения:
Вызов метода print() просто напечатает «null», но не вызовет исключения. Точно так же process() никогда не вернет null в своем ответе. Скорее выдает исключение.
public void print(Object param) {
System.out.println("Printing " + param);
}
public Object process() throws Exception {
Object result = doSomething();
if (result == null) {
throw new Exception("Processing fail. Got a null response");
} else {
return result;
}
}
Таким образом, для клиентского кода, обращающегося к вышеуказанным API, нет необходимости в нулевой проверке.
Однако такие API должны явно указать это в своем контракте. Обычно API-интерфейсы публикуют такой контракт в Javadoc.
Но это не дает четкого указания на контракт API и, таким образом, зависит от разработчиков клиентского кода, чтобы обеспечить его соответствие.
В следующем разделе мы увидим, как некоторые IDE и другие инструменты разработки помогают разработчикам в этом.
4. Автоматизация контрактов API
4.1. Использование статического анализа кода
Инструменты статического анализа кода помогают значительно улучшить качество кода. И несколько таких инструментов также позволяют разработчикам поддерживать нулевой контракт. Одним из примеров является FindBugs.
FindBugs помогает управлять нулевым контрактом с помощью аннотаций @Nullable и @NonNull. Мы можем использовать эти аннотации для любого метода, поля, локальной переменной или параметра. Это делает явным для клиентского кода, может ли аннотированный тип быть нулевым или нет.
Давайте рассмотрим пример:
Здесь @NonNull дает понять, что аргумент не может быть нулевым. Если клиентский код вызывает этот метод без проверки аргумента на значение null, FindBugs выдаст предупреждение во время компиляции.
public void accept(@NonNull Object param) {
System.out.println(param.toString());
}
4.2. Использование поддержки IDE
Разработчики обычно полагаются на IDE для написания кода Java. И такие функции, как интеллектуальное завершение кода и полезные предупреждения, например, когда переменная не была назначена, безусловно, очень помогают.
Некоторые IDE также позволяют разработчикам управлять контрактами API и, таким образом, устраняют необходимость в инструменте статического анализа кода. IntelliJ IDEA предоставляет аннотации @NonNull и @Nullable.
«Чтобы добавить поддержку этих аннотаций в IntelliJ, нам нужно добавить следующую зависимость Maven:
Теперь IntelliJ будет генерировать предупреждение, если отсутствует проверка null, как в нашем последнем примере.
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>16.0.2</version>
</dependency>
IntelliJ также предоставляет аннотацию Contract для обработки сложных контрактов API.
5. Утверждения
До сих пор мы говорили только об устранении необходимости проверки нулей из клиентского кода. Но это редко применимо в реальных приложениях.
Теперь давайте предположим, что мы работаем с API, который не может принимать нулевые параметры или может возвращать нулевой ответ, который должен обрабатываться клиентом. Это требует от нас проверки параметров или ответа на нулевое значение.
Здесь мы можем использовать Java Assertions вместо традиционного условного оператора проверки на null:
В строке 2 мы проверяем параметр на null. Если утверждения включены, это приведет к ошибке AssertionError.
public void accept(Object param){
assert param != null;
doSomething(param);
}
Хотя это хороший способ утверждения предварительных условий, таких как ненулевые параметры, у этого подхода есть две основные проблемы:
Поэтому программистам не рекомендуется использовать утверждения для проверки условий. В следующих разделах мы обсудим другие способы обработки нулевых проверок.
- Assertions are usually disabled in a JVM.
- A false assertion results in an unchecked error that is irrecoverable.
6. Избегание проверки нулей с помощью методов кодирования
6.1. Предварительные условия
Обычно рекомендуется писать код, который дает сбой раньше. Таким образом, если API принимает несколько параметров, которые не могут быть нулевыми, лучше проверять каждый ненулевой параметр в качестве предварительного условия API.
Давайте рассмотрим два метода — один, который дает сбой раньше, а другой — нет:
Очевидно, что мы должны предпочесть goodAccept(), а не badAccept().
public void goodAccept(String one, String two, String three) {
if (one == null || two == null || three == null) {
throw new IllegalArgumentException();
}
process(one);
process(two);
process(three);
}
public void badAccept(String one, String two, String three) {
if (one == null) {
throw new IllegalArgumentException();
} else {
process(one);
}
if (two == null) {
throw new IllegalArgumentException();
} else {
process(two);
}
if (three == null) {
throw new IllegalArgumentException();
} else {
process(three);
}
}
В качестве альтернативы мы также можем использовать предварительные условия Guava для проверки параметров API.
6.2. Использование примитивов вместо классов-оболочек
Поскольку null не является приемлемым значением для таких примитивов, как int, мы должны по возможности предпочесть их аналогам-оболочкам, таким как Integer.
Рассмотрим две реализации метода, который суммирует два целых числа:
Теперь давайте вызовем эти API в нашем клиентском коде:
public static int primitiveSum(int a, int b) {
return a + b;
}
public static Integer wrapperSum(Integer a, Integer b) {
return a + b;
}
Это приведет к ошибке времени компиляции, поскольку null не является допустимое значение для int.
int sum = primitiveSum(null, 2);
А при использовании API с классами-оболочками мы получаем исключение NullPointerException:
Существуют и другие факторы использования примитивов поверх оболочек, как мы рассмотрели в другом руководстве, Java Primitives Versus Objects.
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
6.3. Пустые коллекции
Иногда нам нужно вернуть коллекцию в качестве ответа от метода. Для таких методов мы всегда должны пытаться возвращать пустую коллекцию вместо null:
Таким образом, мы избегаем необходимости, чтобы наш клиент выполнял проверку null при вызове этого метода.
public List<String> names() {
if (userExists()) {
return Stream.of(readName()).collect(Collectors.toList());
} else {
return Collections.emptyList();
}
}
7. Использование объектов
В Java 7 появился новый API объектов. Этот API имеет несколько статических служебных методов, которые избавляются от большого количества избыточного кода.
Давайте рассмотрим один из таких методов, requireNonNull():
Теперь давайте проверим метод accept():
public void accept(Object param) {
Objects.requireNonNull(param);
// doSomething()
}
Итак, если в качестве аргумента передается null, accept() выдает Исключение нулевого указателя.
assertThrows(NullPointerException.class, () -> accept(null));
Этот класс также имеет методы isNull() и nonNull(), которые можно использовать в качестве предикатов для проверки объекта на нуль.
8. Использование дополнительных
8.1. Использование orElseThrow
В Java 8 появился новый необязательный API в языке. Это предлагает лучший контракт для обработки необязательных значений по сравнению с нулевым значением.
Давайте посмотрим, как Optional избавляет от необходимости проверки null:
Возвращая Optional, как показано выше, метод процесса дает понять вызывающей стороне, что ответ может быть пустым и его нужно обработать. во время компиляции.
public Optional<Object> process(boolean processed) {
String response = doSomething(processed);
if (response == null) {
return Optional.empty();
}
return Optional.of(response);
}
private String doSomething(boolean processed) {
if (processed) {
return "passed";
} else {
return null;
}
}
Это заметно устраняет необходимость любых проверок null в клиентском коде. Пустой ответ можно обработать по-разному, используя декларативный стиль Дополнительного API:
Кроме того, он также предоставляет лучший контракт разработчикам API, чтобы показать клиентам, что API может возвращать пустой ответ.
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
«Хотя мы устранили необходимость нулевой проверки вызывающей стороны этого API, мы использовали ее для возврата пустого ответа.
Чтобы избежать этого, Optional предоставляет метод ofNullable, который возвращает Optional с указанным значением или пустым, если значение равно null:
8.2. Использование option с коллекциями
public Optional<Object> process(boolean processed) {
String response = doSomething(processed);
return Optional.ofNullable(response);
}
При работе с пустыми коллекциями опционал пригодится:
Предполагается, что эта функция возвращает первый элемент списка. Функция findFirst Stream API вернет пустой необязательный параметр, если данных нет. Здесь мы использовали orElse вместо значения по умолчанию.
public String findFirst() {
return getList().stream()
.findFirst()
.orElse(DEFAULT_VALUE);
}
Это позволяет нам обрабатывать как пустые списки, так и списки, в которых после использования метода фильтрации библиотеки Stream нет элементов для предоставления.
В качестве альтернативы, мы также можем позволить клиенту решить, как обрабатывать пустое значение, возвращая необязательный параметр из этого метода:
Поэтому, если результат getList пуст, этот метод вернет клиенту пустой необязательный параметр. .
public Optional<String> findOptionalFirst() {
return getList().stream()
.findFirst();
}
Использование Optional с коллекциями позволяет нам разрабатывать API, которые обязательно возвращают значения, отличные от null, что позволяет избежать явных проверок null на клиенте.
Здесь важно отметить, что эта реализация основана на том, что getList не возвращает null. Однако, как мы обсуждали в предыдущем разделе, часто лучше возвращать пустой список, а не ноль.
8.3. Объединение необязательных значений
Когда мы начинаем заставлять наши функции возвращать необязательные значения, нам нужен способ объединить их результаты в одно значение.
Давайте возьмем наш пример с getList из предыдущего. Что, если бы он возвращал необязательный список или должен был быть обернут методом, который обертывал нулевое значение с помощью опционального, используя ofNullable?
Наш метод findFirst хочет вернуть необязательный первый элемент списка необязательных элементов:
Используя функцию flatMap для необязательных элементов, возвращенных из getOptional, мы можем распаковать результат внутреннего выражения, которое возвращает необязательные элементы. Без flatMap результатом будет Optional\u003cOptional\u003cString\u003e\u003e. Операция flatMap выполняется только в том случае, если необязательный не пуст.
public Optional<String> optionalListFirst() {
return getOptionalList()
.flatMap(list -> list.stream().findFirst());
}
9. Библиотеки
9.1. Использование Lombok
Lombok — отличная библиотека, которая уменьшает количество шаблонного кода в наших проектах. Он поставляется с набором аннотаций, которые заменяют общие части кода, которые мы часто пишем сами в приложениях Java, такие как геттеры, сеттеры и toString(), и это лишь некоторые из них.
Другая его аннотация — @NonNull. Таким образом, если проект уже использует Lombok для устранения стандартного кода, @NonNull может заменить необходимость проверки нулей.
Прежде чем мы перейдем к некоторым примерам, давайте добавим зависимость Maven для Lombok:
Теперь мы можем использовать @NonNull везде, где требуется проверка на null:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
Итак, мы просто аннотировали объект, для которого потребовалась бы проверка null, и Lombok генерирует скомпилированный класс:
public void accept(@NonNull Object param){
System.out.println(param);
}
Если param равен null, этот метод генерирует исключение NullPointerException. Метод должен сделать это явным в своем контракте, а клиентский код должен обработать исключение.
public void accept(@NonNull Object param) {
if (param == null) {
throw new NullPointerException("param");
} else {
System.out.println(param);
}
}
9.2. Использование StringUtils
Как правило, проверка строк включает в себя проверку пустого значения в дополнение к нулевому значению.
Таким образом, это будет общий оператор проверки:
Это быстро становится излишним, если нам приходится иметь дело с большим количеством типов String. Вот где StringUtils пригодится.
public void accept(String param){
if (null != param && !param.isEmpty())
System.out.println(param);
}
Прежде чем мы увидим это в действии, давайте добавим зависимость Maven для commons-lang3:
Теперь давайте рефакторим приведенный выше код с помощью StringUtils:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
Итак, мы заменили нашу нулевую или пустую проверку со статическим служебным методом isNotEmpty(). Этот API предлагает другие мощные служебные методы для обработки общих функций String.
public void accept(String param) {
if (StringUtils.isNotEmpty(param))
System.out.println(param);
}
10. Заключение
В этой статье мы рассмотрели различные причины NullPointerException и почему их трудно идентифицировать.
Затем мы рассмотрели различные способы избежать избыточности в коде, связанного с проверкой нулевого значения с параметрами, типами возвращаемых значений и другими переменными.
Все примеры доступны на GitHub.
«