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

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
Share post

Similar posts

Comments 28

    +3
    Почему Вами были выбраны именно аннотации intellij?
    Насколько я помню, с помощью них можно обнаружить ошибки только на этапе компиляции. Для отлова NPE в runtime в одном из проектов мною использовались javax.validation.constraints.
      0
      Idea только проверяет корректность, в ней указываются маркеры-аннотации для проверок. Автор же использует аннотации из findbugs, но можно свои написать и использовать с тем же успехом.
        0
        Да, верно. Просто эти уже используются в guava и guice. Первую я использую вообще во всех своих проектах, а второй там, где spring stack избыточен.
        +1
        Скорей всего вы использовали их для Design by Contract. Помнится имплементация Hibernate Validation для первой версии стандарта так умела. Это другой подход, решающий несколько иную задачу. Здесь же речь идет не о ловле ошибки в runtime, а о том чтобы ее туда вообще не пропустить.

        Java Bean Validation тоже активно использовал там, где это уместно. В плане обязательности/необязательности у них не нравится, что по-умолчанию все считается Nullable и нельзя это поменять.
          0
          При запуске кода из-под IDEA проверки работают и в runtime'е, т.к. она сама добавляет в нужные места Assert'ы
          … с помощью них можно обнаружить ошибки только на этапе компиляции...

          это их основная прелесть, обнаружение ошибки в рантайме может дорого стоит )
          +1
          Стоило упомянуть и сам FindBugs, который помимо использования этих аннотаций анализирует легаси-код и сам размечает неразмеченные функции, порой делая очень нетривиальные выводы. Если в функции аргумент сравнивается с null, он помечается Nullable. Если сразу разыменовывается, но помечается NotNull и так далее. Это работает рекурсивно: если вы получив аргумент первым делом передаёте его в фунцкию, в которой он первым делом разыменовывается, то и для текущей функции аргумент будет NotNull. У меня FindBugs реально отлавливал потенциальные NPE на глубине в три вызова в неразмеченном аннотациями коде. Кроме того, часть методов стандартной библиотеки (включая методы интерфейсов) захардкожена в FindBugs, если по документации известно, Nullable они или нет. Благодаря мне вот парочка появилась :-)

          Nullable/NotNull серьёзно помогают, но с ними есть трудности, когда работаешь с коллекциями или массивами. Было бы круто иметь отдельную аннотацию, например, «массив ненулевых элементов».
            0
            Последнее время мне как-то везло либо на новые проекты, либо с уже какой-то внедренной политикой обработки опциональности. Поэтому посмотреть внешние стат. анализаторы с одной стороны все хочется, а с другой стороны как-то не особо и надо. Спасибо, что напомнили про FindBugs, возможно в текущем проекте это как раз то, что доктор прописал. А там как практика с ним появится, так и упомяну. :-)

            Для коллекций вроде как TypeAnnotations должны эту проблему решить, так же как и для прочих Generic'ов типа Future<@Nullable User>.
            0
            В Java 8 и в библиотеках типа Guava есть тип Optional. Часть NPE закрывается использованием Optional.ofNullable().
              +2
              Это фактически монада MayBe. Чтобы прижилось нужны лямбды, да и сама реализация мне пока только в Scala понравилась (может за счет pattern matching'а). А так да, появляются compile time проверки на опциональность. Правда за все хорошее приходится платить и в runtime будет overhead.
                0
                Да, она и есть.
                  0
                  В Котлине реализация куда лучше на мой взгляд. Она там практически зеро-оверхед в плане количества кода. И это свойство языка там.
                  А в Java 8 тоже есть лямбды и Optional отлично выглядит. Собственно у себя в проекте его и используем.
                0
                Это все здорово, но не подскажите методы борьбы с NPE при method chaining и legacy кодом?
                Например есть такая ужасная цепочка вызовов getOffer().getOrderActionRef().getOrder().getRootCustomer().getCode() NPE может быть где-угодно, ловить NPE как-то рука не поднимается.
                  +3
                  Раньше я тоже думал, что chaining — это проблема, но опыт показал, что в таких цепочках возврат null — нештатная ситуация и лучше кидать явное исключение бизнес-уровня, чем возвращать null. В вашем примере мне не очень понятна логика методов, поэтому сочиню свой:
                  long getUserQuota(String userName, String resourceName) {
                    return getDatabaseEngine().getUserInfo(userName).getResourceLimits(resourceName).getQuota();
                  }

                  Когда может getDatabaseEngine() вернуть null? Когда произошло что-то фатальное, нет соединения с базой, не удалось загрузить jar с драйвером JDBC и т. д. Не лучше ли кинуть что-то типа new DatabaseInitializationException(), которое выложит в лог подробную историю, а пользователю покажет «извините, фатальная ошибка, позвоните в саппорт»?
                  Когда может getUserInfo(userName) вернуть null? Наверно, когда такого юзера нет в базе. Но мы же как-то пришли в эту функцию с этим параметром. Либо юзер залогинен, либо мы выбрали его из комбобокса в админке. Возможно, другой админ в это время удалил этого же юзера. Это редкая и нештатная ситуация. Спокойно кидаем new UserNotFoundException(userName), который превратится в красивое сообщение в UI. Что-то аналогичное и с последним вызовом.

                  Во многих случаях кинуть конкретное информативное исключение, содержащее не только стек, но и важные для контекста параметры значительно лучше, чем вернуть null. В тех редких случаях, когда возврат null может оказаться штатной ситуацией и оборачивать в try-catch некрасиво, я пишу парный метод типа optUserInfo, который никогда не кидает исключений, но может вернуть null (навеяно библиотекой org.json).

                  Кстати, считаю Optional страшным извратом, который загрязняет код. Всё решается правильной и последовательной системой исключений.
                    0
                    Я бы сделал так. Если непонятно где возврат null — штатная ситуация, то сначала анализировать и аннотировать. Затем все таки явно обрабатывать null там где они допустимы. Всякие удобные операторы типа .? из Groovy и Kotlina могут проглатывать null там где его быть не должно, но он вызван переходом программы в недопустимое состояние (IllegalState). В этом случае лучше все таки явно падать, а не продолжать работу.
                    0
                    Ради справедливости надо упомянуть, что в Eclipse поддержка null-аннтотаций тоже есть.
                    Причём можно выбрать классы и из JSR, и из IDEA, так и свои собственные эклипсовские.
                      +1
                      раньше не использовал nullable, а в чем основной профит?
                             @Nullable User foundUser = findUserByName("vasya");
                      
                              if(foundUser == null) {
                                  System.out.println("User not found");
                                  return;
                              }
                      

                      то есть проверка на null никуда не ушла, а @Nullable — типа доки/коммента что из метода может вернуться null?
                        +1
                        не только вернуться, но так же и передаться в метод

                        public void setParam(@Nullable Object param){
                        // Тут уже можно делать или не делать проверку на param==null в зависимости от логики метода.
                        // Но нормальная IDE или Findbugs так же сделают инспекцию на передаваемое значение, проверив, что нигде в метод не будет попыток передать null
                        }

                        public void setParam(@Nullable Object param){
                        // Тут уже можно делать или не делать проверку на param==null в зависимости от логики метода.
                        // Но нормальная IDE или Findbugs так же сделают инспекцию на передаваемое значение с учётом что можно передать и null
                        }


                        дополнительное чтиво: habrahabr.ru/post/139736/#comment_4670467
                          +1
                          выше копипаста очепятная была. первый блок должен так выглядеть:
                          public void setParam(@NotNull Object param){
                          // Тут уже можно делать или не делать проверку на param==null в зависимости от логики метода.
                          // Но нормальная IDE или Findbugs так же сделают инспекцию на передаваемое значение, проверив, что нигде в метод не будет попыток передать null
                          }
                          +1
                          Основной профит становится заметен, когда используете обе аннотации: @Nonnull, @Nullable.
                          Если у вас есть переменная @Nullable и метод, который принимает @Nonnull переменную, IDE и Findbugs покажут вам это место.
                          Разумеется, в одинокой @Nonnull аннотации смысла нет.
                            +1
                            В указанном примере задание @Nullable для переменной излишне, т.к. IDE и так знает, что метод findUserByName может вернуть null. Помечать необходимо все методы, параметры методов и поля классов. Тогда, как показывает практика, NPE уходит из вашей жизни почти полностью.
                              +1
                              В случае метода — это контракт на «опциональность» между вызывающим и вызываемым. Для объекта и его полей — это инвариант. Даже без всяких статических анализаторов, просто прописав их, вы сможете рассматривать гораздо меньше вариантов состояния и поведения приложения при написании вашего кода.

                              Ну, и естественно, не надо рыскать по всему коду на тему «а может ли прийти вот сюда null» или строить сомнительные предположения.
                                0
                                остается вопрос — почему @Nullable не используется в крупных проектах? Сталкивался с сорцами Android и ElasticSearch — ни одного @Nullable. В чем причина?
                                  0
                                  Ну, например, в Jetbrains, используют. А так помнится linux kernell был вообще без тестов много лет, ситуация как-то начала меняться совсем недавно. Что вовсе не означает, что так стоит жить. :-)
                                0
                                Как насчет @Contract?
                                  0
                                  Честно говоря, выглядит уже не так интересно в плане затрачиваемых ресурсов/приносимой пользы. Да и Intellij Idea specific, как я понимаю. Лучше добавьте поддержку «аннотирования» внешних библиотек. ;-)
                                    0
                                    Вы про External-Annotations? :)
                                      0
                                      Блин, а ведь и правда. По Alt+Enter на внешнем коде предлагает его проаннотировать.

                                      В заблуждение ввела сначала вот эта часть доки:

                                      If an annotation pertains to the SDK, configured for your project, the path is to be defined in the SDK settings. If an annotation pertains to a module, the path should be defined in the module settings.

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

                                      Вообще круто, но в feature requests я бы добавил suggestion на добавление external annotation (если они включены) в подобных случаях:
                                      Map<String, String> map = ImmutableMap.of("bca", "cab"); @Nullable String str = map.get("abc");

                                      Ну, и какое-то отображение в исходниках проаннотированной библиотеки, кроме Ctrl+Q. Хотя и с трудом представляю себе как. Хитрые подчеркивания разве что.

                              Only users with full accounts can post comments. Log in, please.