Побеждаем NPE hell в Java 6 и 7, используя Intellij Idea

в 11:36, , рубрики: intellij idea, java

Disclaimer

  • Статья не претендует на открытие Америки и носит популяризаторско-реферативный характер. Способы борьбы с NPE в коде далеко не новые, но намного менее известные, чем этого хотелось бы.
  • Разовый NPE — это, наверное, самая простая из все возможных ошибок. Речь идет именно о ситуации, когда из-за отсутствия политики их обработки наступает засилье NPE.
  • В статье не рассматриваются подходы, не применимые для Java 6 и 7 (монада MayBe, JSR-308 и Type Annotations).
  • Повсеместное защитное программирование не рассматривается в качестве метода борьбы, так как сильно замусоривает код, снижает производительность и в итоге все равно не дает нужного эффекта.
  • Возможны некоторые расхождения в используемой терминологии и общепринятой. Так же описание используемых проверок Intellij Idea не претендует на полноту и точность, так как взято из документации и наблюдаемого поведения, а не исходного кода.

JSR-305 спешит на помощь

Здесь я хочу поделиться используемой мной практикой, которая помогает мне успешно писать почти полностью NPE-free код. Основная ее идея состоит в использовании аннотаций о необязательности значений из библиотеки, реализующей JSR-305 (com.google.code.findbugs: jsr305: 1.3.9):

  • @Nullable — аннотированное значение является необязательным;
  • @Nonnull — соответственно наоборот.

Естественно обе аннотации применимы к полям объектов и классов, аргументам и возвращаемым значениям методов, локальным переменным. Таким образом эти аннотации дополняют информацию о типе в части обязательности наличия значения.

Но аннотировать все подряд долго и читаемость кода резко снижается. Поэтому, как правило, команда проекта принимает соглашение о том, что все, что не помечено @Notnull, является обязательным. С этой практикой хорошо знакомы те, кто использовал Guava, Guice.

Вот пример возможного кода такого абстрактного проекта:

import javax.annotation.Nullable;

public abstract class CodeSample {

    public void correctCode() {
        @Nullable User foundUser = findUserByName("vasya");

        if(foundUser == null) {
            System.out.println("User not found");
            return;
        }

        String fullName = Asserts.notNull(foundUser.getFullName());
        System.out.println(fullName.length());
    }

    public abstract @Nullable User findUserByName(String userName);

    private static class User {
        private String name;
        private @Nullable String fullName;

        public User(String name, @Nullable String fullName) {
            this.name = name;
            this.fullName = fullName;
        }

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }

        @Nullable public String getFullName() { return fullName; }
        public void setFullName(@Nullable String fullName) { this.fullName = fullName; }
    }
}

Как видно везде понятно можно ли получить null при дереференсе ссылки.

Единственный нюанс состоит в том, что возникают ситуации, когда в текущем контексте (н-р, на определенном этапе бизнес-процесса) мы точно знаем, что что-то в общем случае необязательное должно присутствовать. В нашем случае это полное имя Василия, которое может в принципе и отсутствовать у пользователя, но мы то знаем, что здесь и сейчас это невозможно согласно правилам бизнес логики. Для таких ситуаций я использую простую assert-утилиту:

import javax.annotation.Nullable;

public class Asserts {
    /**
     * For situations, when we definitely know that optional value cannot be null in current context.
     */
    public static <T> T notNull(@Nullable T obj) {
        if(obj == null) {
            throw new IllegalStateException();
        }
        return obj;
    }
}

Настоящие java asserts тоже можно использовать, но у меня они не прижились из-за необходимости явного включения в runtime и менее удобного синтаксиса.

Пара слов про наследование и ковариантность/контравариантность:

  • если возвращаемый тип метода предка является NotNull, то переопределенный метод наследника тоже должен быть NotNull. Остальное допустимо;
  • если аргумент метода предка является Nullable, то переопределенный метод наследника тоже должен быть Nullable. Остальное допустимо.

На самом деле этого уже вполне достаточно и статический анализ (в IDE или на CI) не особо нужен. Но пускай и IDE поработает, не зря же покупали. Я предпочитаю использовать Intellij Idea, поэтому все дальнейшие примеры будут по ней.

Intellij Idea делает жизнь лучше

Сразу скажу, что по-умолчанию Idea предлагает свои аннотации с аналогичной семантикой, хотя и понимает все остальные. Изменить это можно в Settings -> Inspections -> Probable bugs -> {Constant conditions & exceptions; @NotNull/@Nullable problems}. В обеих инспекциях нужно выбрать используемую пару аннотаций.

Вот как в Idea выглядит подсветка ошибок, найденных инспекциями, в некорректном варианте реализации предыдущего кода:
Побеждаем NPE hell в Java 6 и 7, используя Intellij Idea

Стало совсем замечательно, IDE не только находит два NPE, но и вынуждает нас с ними что-то сделать.

Казалось бы все хорошо, но встроенный статический анализатор Idea не понимает принятого нами соглашения об обязательности по-умолчанию. С ее точки зрения (как и любого другого стат. анализатора) здесь появляется три варианта:

  • Nullable — значение обязательно;
  • NotNull — значение необязательно;
  • Unknown — про обязательность значения ничего не известно.

И все что мы не стали размечать теперь считается Unknown. Является ли это проблемой? Для ответа на этот вопрос необходимо понять что же умеют находить инспекции Idea для Nullable и NotNull:

  • dereference переменной, потенциально содержащей null, при обращении к полю или методу объекта;
  • передача в NotNull аргумент Nullable переменной;
  • избыточная проверка на отсутствие значения для NotNull переменной;
  • не соответствие параметров обязательности при присвоении значения;
  • возвращение NotNull методом Nullable переменной в одной из веток.

Логично, что любое значение, возвращенное из метода библиотеки, не размеченной данными аннотациями является Unknown. Для борьбы с этим достаточно просто пометить аннотацией локальную переменную или поле, которым осуществляется присваивание.

Если мы продолжаем придерживаться нашей практики, то в нашем коде останется помечено как Nullable все необязательное. Таким образом первая проверка продолжает работать, защищая нас от многих NPE. К сожалению, все остальные проверки отвалились. Не работает в том числе и вторая проверка, крайне полезная против товарищей, очень любящих как писать методы, активно принимающие null в качестве аргументов, так и передавать null в чужие методы, не рассчитанные на это.

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

  • в настройках инспекции «Constant conditions & exceptions» активировать опцию «Suggest @Nullable annotation for methods that may possibly return null and report nullable values passed to non-annotated parameters». Это приведет к тому, что все неаннотированные аргументы методов по всему проекту будут считаться NotNull. Для только начинающегося проекта это решение отлично подойдет, но по понятным причинам оно не уместно при внедрении практики в проект с значетильной существующей кодовой базой;
  • использовать аннотацию @ParametersAreNonnullByDefault для задания соответствующего поведения в определенном scope, которым может быть метод, класс, пакет. Это решение уже отлично подходит для legacy проекта. Ложкой дегтя является то, что при задании поведения для пакета рекурсия не поддерживается и на весь модуль за один раз эту аннотацию не навесить.

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

Ближайшее будущее

Улучшить ситуацию призвана грядущая поддержка @TypeQualifierDefault, которая уже работает в Intellij Idea 14 EAP. С помощью них можно определить свою аннотацию @NonNullByDefault, которая будет определять обязательность по-умолчанию для всего, поддерживая те же scopes. Рекурсивности сейчас тоже нет, но дебаты идут.

Ниже продемонстрировано как выглядят инспекции для трех случаев работы из legacy кода с кодом в новом стиле с аннотациями.

Аннотируем явно:

Побеждаем NPE hell в Java 6 и 7, используя Intellij Idea

По-умолчанию только аргументы:

Побеждаем NPE hell в Java 6 и 7, используя Intellij Idea

По-умолчанию все:

Побеждаем NPE hell в Java 6 и 7, используя Intellij Idea

Конец

Вот теперь все стало почти замечательно, осталось дождаться выхода Intellij Idea 14. Единственное, чего еще не хватает до полного счастья — это возможности добавления такой метаинформации для внешних библиотек в какой-нибудь external xml. Помнится такую функциональность поддерживали родные аннотации Intellij Idea, правда только для JDK. Ну, и еще нельзя аннотировать тип в Generic без поддержки Type annotations из Java 8. Чего очень не хватает для ListenableFutures и коллекций в редких случаях.

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

Использованные источники

  1. stackoverflow.com/questions/16938241/is-there-a-nonnullbydefault-annotation-in-idea
  2. www.jetbrains.com/idea/webhelp/annotating-source-code.html
  3. youtrack.jetbrains.com/issue/IDEA-65566
  4. youtrack.jetbrains.com/issue/IDEA-125281

Автор: tr1cks

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js