Pull to refresh

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

Reading time6 min
Views35K

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 — соответственно наоборот.

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

Но аннотировать все подряд долго и читаемость кода резко снижается. Поэтому, как правило, команда проекта принимает соглашение о том, что все, что не помечено @Nullable, является обязательным. С этой практикой хорошо знакомы те, кто использовал 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 выглядит подсветка ошибок, найденных инспекциями, в некорректном варианте реализации предыдущего кода:


Стало совсем замечательно, 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 кода с кодом в новом стиле с аннотациями.

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



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



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



Конец


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

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

upd. Добавлять метаинформацию об опциональности значения для внешних библиотек оказывается все таки можно.

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


  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
Tags:
Hubs:
+13
Comments28

Articles