Рубрики
Без рубрики

Избегайте проверки оператора Null в Java

Изучите несколько стратегий,позволяющих избежать слишком знакомых шаблонных условных операторов для проверки нулевых значений в Java.

Автор оригинала: baeldung.

1. Обзор

Как правило, null переменные, ссылки и коллекции сложно обрабатывать в коде Java. Их не только трудно идентифицировать, но и с ними сложно иметь дело.

На самом деле, любая ошибка в работе с null не может быть идентифицирована во время компиляции и приводит к исключению NullPointerException во время выполнения.

В этом уроке мы рассмотрим необходимость проверки null в Java и различные альтернативы, которые помогут нам избежать проверок null в нашем коде.

Дальнейшее чтение:

Использование Null Away, чтобы избежать NullPointerExceptions

Примечания по безопасности пружины

Введение в шаблон нулевого объекта

2. Что Такое Исключение NullPointerException?

Согласно Javadoc для NullPointerException , он возникает, когда приложение пытается использовать null в случае, когда требуется объект, например:

  • Вызов метода экземпляра объекта null
  • Доступ или изменение поля объекта null
  • Принимая длину null , как если бы это был массив
  • Доступ или изменение слотов null , как если бы это был массив
  • Бросание null , как если бы это было Выбрасываемое значение

Давайте быстро рассмотрим несколько примеров кода Java, которые вызывают это исключение:

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // success
    }
}

private String doSomethingElse() {
    return null;
}

Здесь мы попытались вызвать вызов метода для ссылки null . Это приведет к исключению NullPointerException.

Другой распространенный пример-если мы попытаемся получить доступ к массиву null :

public static void main(String[] args) {
    findMax(null);
}

private static void findMax(int[] arr) {
    int max = arr[0];
    //check other elements in loop
}

Это вызывает исключение NullPointerException в строке 6.

Таким образом, доступ к любому полю, методу или индексу объекта null вызывает исключение NullPointerException , как видно из приведенных выше примеров.

Распространенный способ избежать исключения NullPointerException – проверить наличие null :

public void doSomething() {
    String result = doSomethingElse();
    if (result != null && result.equalsIgnoreCase("Success")) {
        // success
    }
    else
        // failure
}

private String doSomethingElse() {
    return null;
}

В реальном мире программистам трудно определить, какие объекты могут быть null. Агрессивно безопасной стратегией было бы проверить null для каждого объекта. Это, однако, вызывает множество избыточных проверок null и делает наш код менее читаемым.

В следующих нескольких разделах мы рассмотрим некоторые альтернативы в Java, которые позволяют избежать такой избыточности.

3. Обработка null Через контракт API

Как обсуждалось в предыдущем разделе, доступ к методам или переменным объектов null вызывает исключение NullPointerException. Мы также обсуждали, что установка null проверки объекта перед доступом к нему исключает возможность исключения NullPointerException.

Однако часто существуют API, которые могут обрабатывать значения 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;
    }
}

Вызов метода print() просто выведет “null” , но не вызовет исключения. Аналогично, process() никогда не вернет null в своем ответе. Это скорее вызывает исключение .

Таким образом, для клиентского кода, обращающегося к вышеуказанным API, нет необходимости в проверке null .

Однако такие API должны четко указывать это в своем контракте. Общим местом для API для публикации такого контракта является JavaDoc .

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

В следующем разделе мы увидим, как несколько идей и других инструментов разработки помогают разработчикам в этом.

4. Автоматизация контрактов API

4.1. Использование Статического Анализа Кода

Инструменты статического анализа кода помогают значительно улучшить качество кода. И несколько таких инструментов также позволяют разработчикам поддерживать контракт null . Одним из примеров является FindBugs .

FindBugs помогает управлять контрактом null с помощью аннотаций @Nullable и @NonNull . Мы можем использовать эти аннотации над любым методом, полем, локальной переменной или параметром. Это делает явным для клиентского кода, может ли аннотированный тип быть null или нет. Давайте рассмотрим пример:

public void accept(@Nonnull Object param) {
    System.out.println(param.toString());
}

Здесь @NonNull дает понять, что аргумент не может быть null. Если клиентский код вызывает этот метод без проверки аргумента на null, FindBugs выдаст предупреждение во время компиляции.

4.2. Использование поддержки IDE

Разработчики обычно полагаются на идеи для написания кода Java. И такие функции, как интеллектуальное завершение кода и полезные предупреждения, например, когда переменная, возможно, не была назначена, безусловно, в значительной степени помогают.

Некоторые IDE также позволяют разработчикам управлять контрактами API и тем самым устраняют необходимость в инструменте статического анализа кода. IntelliJ IDEA предоставляет аннотации @NonNull и @Nullable . Чтобы добавить поддержку этих аннотаций в IntelliJ, мы должны добавить следующую зависимость Maven:


    org.jetbrains
    annotations
    16.0.2

Теперь IntelliJ выдаст предупреждение, если проверка null отсутствует, как в нашем последнем примере.

IntelliJ также предоставляет аннотацию Contract для обработки сложных контрактов API.

5. Утверждения

До сих пор мы говорили только об устранении необходимости проверки null из клиентского кода. Но это редко применимо в реальных приложениях.

Теперь давайте предположим, что мы работаем с API, который не может принимать null параметры или может возвращать null ответ, который должен быть обработан клиентом . Это приводит к необходимости проверки параметров или ответа на значение null .

Здесь мы можем использовать утверждения Java вместо традиционного условного оператора null check:

public void accept(Object param){
    assert param != null;
    doSomething(param);
}

В строке 2 мы проверяем наличие параметра null . Если утверждения включены, это приведет к ошибке AssertionError.

Хотя это хороший способ утверждения предварительных условий, таких как не- null параметры, у этого подхода есть две основные проблемы :

  1. Утверждения обычно отключаются в JVM
  2. Утверждение false приводит к непроверенной ошибке, которую невозможно исправить

Следовательно, программистам не рекомендуется использовать утверждения для проверки условий. В следующих разделах мы обсудим другие способы обработки null валидаций.

6. Избегание Нулевых Проверок С Помощью Методов Кодирования

6.1. Предварительные условия

Обычно рекомендуется писать код, который рано выходит из строя. Поэтому, если API принимает несколько параметров, которые не могут быть null , лучше проверить каждый параметр, не являющийся null , в качестве предварительного условия API.

Например, давайте рассмотрим два метода – один, который рано терпит неудачу, и другой, который этого не делает:

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);
    }
}

Очевидно, что мы должны предпочесть good Accept() над bad Accept().

В качестве альтернативы мы также можем использовать предварительные условия Guava для проверки параметров API.

6.2. Использование примитивов вместо классов-оболочек

Поскольку null не является приемлемым значением для примитивов, таких как int, мы должны предпочесть их аналогам-оболочкам, таким как Integer , где это возможно.

Рассмотрим две реализации метода, который суммирует два целых числа:

public static int primitiveSum(int a, int b) {
    return a + b;
}

public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}

Теперь давайте назовем эти API в нашем клиентском коде:

int sum = primitiveSum(null, 2);

Это приведет к ошибке во время компиляции, так как нулевой не является допустимым значением для инт.

И при использовании API с классами-оболочками мы получаем NullPointerException :

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

Существуют и другие факторы для использования примитивов поверх оберток, как мы рассмотрели в другом учебнике, Примитивы Java по сравнению с объектами .

6.3. Пустые коллекции

Иногда нам нужно вернуть коллекцию в качестве ответа от метода. Для таких методов мы всегда должны пытаться возвращать пустую коллекцию вместо null :

public List names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}

Таким образом, мы избежали необходимости для нашего клиента выполнять проверку null при вызове этого метода.

7. Использование объектов

Java 7 представила новый Objects API. Этот API имеет несколько статических служебных методов, которые удаляют много избыточного кода. Давайте рассмотрим один из таких методов, requireNonNull() :

public void accept(Object param) {
    Objects.requireNonNull(param);
    // doSomething()
}

Теперь давайте протестируем метод accept() :

assertThrows(NullPointerException.class, () -> accept(null));

Таким образом, если null передается в качестве аргумента, accept() вызывает исключение NullPointerException.

Этот класс также имеет методы is Null() и NonNull () , которые могут использоваться в качестве предикатов для проверки объекта на наличие null.

8. Использование Опционально

8.1. Использование orElseThrow

Java 8 представила новый Необязательный API в языке. Это обеспечивает лучший контракт для обработки необязательных значений по сравнению с null. Давайте посмотрим, как Необязательно устраняет необходимость в null проверках:

public Optional 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;
    }
}

Возвращая Необязательный, как показано выше, метод process дает понять вызывающему, что ответ может быть пустым и должен быть обработан во время компиляции.

Это, в частности, устраняет необходимость в любых проверках null в клиентском коде. Пустой ответ может быть обработан по-разному, используя декларативный стиль Необязательного API:

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

Кроме того, он также предоставляет лучший контракт разработчикам API, чтобы показать клиентам, что API может возвращать пустой ответ.

Хотя мы устранили необходимость проверки null на вызывающем объекте этого API, мы использовали его для возврата пустого ответа. Чтобы избежать этого, Optional предоставляет метод ofNullable , который возвращает Optional с указанным значением или empty , если значение null :

public Optional process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

8.2. Использование опционально с коллекциями

При работе с пустыми коллекциями/| Необязательно пригодится:

public String findFirst() {
    return getList().stream()
      .findFirst()
      .orElse(DEFAULT_VALUE);
}

Эта функция должна возвращать первый элемент списка. Функция Stream API findFirst вернет пустой Необязательный при отсутствии данных. Здесь мы использовали или , чтобы вместо этого указать значение по умолчанию.

Это позволяет нам обрабатывать либо пустые списки, либо списки, которые после того, как мы использовали метод Stream библиотеки filter , не имеют элементов для предоставления.

В качестве альтернативы мы также можем позволить клиенту решить, как обрабатывать empty , вернув Необязательно из этого метода:

public Optional findOptionalFirst() {
    return getList().stream()
      .findFirst();
}

Поэтому, если в результате getList пусто, этот метод вернет пустое Необязательный к клиенту.

Использование Необязательно с коллекциями позволяет нам создавать API, которые обязательно возвращают ненулевые значения, избегая, таким образом, явных проверок null на клиенте.

Здесь важно отметить, что эта реализация опирается на get List , не возвращающий null. Однако, как мы обсуждали в предыдущем разделе, часто лучше возвращать пустой список, а не null .

8.3. Комбинирование Необязательно

Когда мы начинаем делать наши функции возвращаемыми Необязательными , нам нужен способ объединить их результаты в одно значение. Давайте возьмем наш пример getList из предыдущего. Что делать, если он должен был вернуть Необязательный список или должен был быть обернут методом, который обернул null с Необязательным использованием ofNullable ?

Наш метод findFirst хочет вернуть Необязательный первый элемент Необязательного списка:

public Optional optionalListFirst() {
   return getOptionalList()
      .flatMap(list -> list.stream().findFirst());
}

С помощью функции flatMap на Optional , возвращаемой из get Optional , мы можем распаковать результат внутреннего выражения, которое возвращает Optional . Без flatMap результатом будет Optional<Необязательная<Строка>> . Операция flatMap выполняется только в том случае, если Необязательный не пуст.

9. Библиотеки

9.1. Использование Ломбока

Lombok – отличная библиотека, которая уменьшает количество шаблонного кода в наших проектах. Он поставляется с набором аннотаций, которые заменяют общие части кода, которые мы часто пишем сами в приложениях Java, таких как геттеры , сеттеры и toString () , чтобы назвать некоторые из них.

Еще одна из его аннотаций – @NonNull. Итак, если проект уже использует Ломбок для устранения шаблонного кода, @NonNull может заменить необходимость в null проверках .

Прежде чем мы перейдем к некоторым примерам, давайте добавим зависимость Maven для Ломбока:


    org.projectlombok
    lombok
    1.18.6

Теперь мы можем использовать @NonNull везде, где требуется проверка null :

public void accept(@NonNull Object param){
    System.out.println(param);
}

Итак, мы просто аннотировали объект, для которого требовалась бы проверка null , и Lombok генерирует скомпилированный класс:

public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}

Если param равен null, этот метод вызывает исключение NullPointerException. Метод должен явно указать это в своем контракте, и клиентский код должен обработать исключение.

9.2. Использование стрингутилов

Как правило, проверка String включает проверку пустого значения в дополнение к значению null . Таким образом, общее утверждение проверки будет:

public void accept(String param){
    if (null != param && !param.isEmpty())
        System.out.println(param);
}

Это быстро становится излишним, если нам приходится иметь дело с большим количеством типов String . Вот где StringUtils пригодится. Прежде чем мы увидим это в действии, давайте добавим зависимость Maven для commons-lang3 :


    org.apache.commons
    commons-lang3
    3.11

Теперь давайте рефакторингуем приведенный выше код с помощью StringUtils:

public void accept(String param) {
    if (StringUtils.isNotEmpty(param))
        System.out.println(param);
}

Итак, мы заменили нашу проверку null или empty методом static utility isNotEmpty(). Этот API предлагает другие мощные служебные методы для обработки общих строковых функций.

10. Заключение

В этой статье мы рассмотрели различные причины NullPointerException и почему это трудно определить. Затем мы увидели различные способы избежать избыточности в коде вокруг проверки null с параметрами, типами возвращаемых значений и другими переменными.

Все примеры доступны на GitHub .