Побеждаем NPE hell в Java, не используя IntelliJ IDEA

    Отказ от ответственности


    Во-первых, материал появился потому, что хотелось быстро объяснить индусам коллегам, что такое null-анализ, чем он хорош и почему нужно прямо сейчас преодолеть корпоративную лень и начать использовать этот анализ в проектах. Статьи, содержащей полную систематизированную информациию о предмете без привязки к IDE, не нашлось, так что пришлось писать самому. И хотя в результате полной систематизации так и не получилось, всё равно захотелось поделиться материалом с более широкой аудиторией. Будет минимум текста и много изображений.

    Во-вторых, на ресурсе уже есть отличная статья tr1cks, посвящённая IntelliJ IDEA, которая усиливает и без того стойкое впечатление, что IDEA — это очень хорошо, а Eclipse — это для бедных (что стало притчей во языцех). Для соблюдения баланса я сконцентрируюсь на Eclipse.

    Я буду использовать аннотации проекта FindBugs (тж. известные как JSR 305-аннотации) по причине отсутствия их привязки к конкретной среде разработки (фанаты могут использовать org.eclipse.jdt.annotation.* и org.jetbrains.annotations.*), а также в силу того, что они доступны в Maven Central. Тем, кто использует Maven, достаточно добавить в раздел <dependencies/> следующее:

    <dependency>
        <groupId>com.google.code.findbugs</groupId>
        <artifactId>jsr305</artifactId>
        <version>3.0.0</version>
    </dependency>
    

    На всякий случай оговариваюсь, что

    	@Nonnull
    	public String getName() {
    		// ...
    	}
    

    и

    	public @Nonnull String getName() {
    		// ...
    	}
    

    — это две абсолютно эквивалентные конструкции, хотя в ряде случаев два варианта полученного байткода могут различаться ровно на 1 байт (кстати, как думаете — почему?)

    Eclipse


    Всё нижеследующее тестировалось на Eclipse 4.4 (Luna). В то же время, если мне не изменяет память, описываемая функциональность уже присутствовала в 3.8 (а возможно, и раньше).

    Настройка


    • Если вы используете модуль M2Eclipse, добавление аннотаций происходит мгновенно:


    • Далее необходимо залезть в глобальные настройки либо настройки проекта, выбрать Java -> Compiler -> Errors/Warnings -> Null analysis и включить Enable annotation-based null analysis.


    • После этого Eclipse спросит, поднять ли уровень проблем Null pointer access и Potential null pointer access до «Error». Здесь стоит иметь в виду, что, согласившись, Вы получите множество ошибок компиляции для существующего кода, так что я бы рекомендовал делать это только для новых проектов, а в противном случае оставлять уровень «Warning».
    • Теперь можно снять флаг Use default annotations for null specifications и выбрать JSR 305-аннотации взамен заводских:


    • Флаг Enable syntactic null analysis for fields стоит выбрать: в этом случае количество ложных срабатываний (а они будут) уменьшится.
    • Флаг Missing '@NonNullByDefault' annotation on package также стоит выбрать (наличие этого флага выгодно отличает Eclipse от IDEA), но аннотировать с помощью @NonNullByDefault или @ParametersAreNonnullByDefault только новые, создаваемые пакеты (огромное количество уже написанного кода, в т. ч. в потрохах JDK, банально не соответствует требованию «ненулевых параметров» и приведёт к ошибкам компиляции). На всякий случай — аннотации на пакет целиком (в файле «package-info.java») «вешаются» так:

      @ParametersAreNonnullByDefault
      package com.example;
      
      import javax.annotation.ParametersAreNonnullByDefault;
      


    Ложноположительный диагноз


    Мало кто обрадуется, если диагноз поставлен ошибочно. К счастью, есть возможность сказать: «Доктор, вы были пьяны и проанализировали чужую кровь!»

    Проблема


    На снимке видно, что Eclipse не в состоянии определить, что (в однопоточном сценарии, разумеется) поле field не может быть null в момент вызова hashCode():


    Да, конечно, мы можем пометить весь метод с помощью @SuppressWarnings("null"), но это сведёт к нулю всю пользу от нуль-анализа.

    Обходной манёвр 0


    Добавим assert:


    Обходной манёвр 1


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


    Подводные камни


    Начиная с Java 1.8, поддерживается новый тип метаданных — type annotations (см. JSR 308) — и, соответственно, от аннотаций требуется явно указывать @Target. Аннотации FindBugs не соответствуют этому требованию. Поэтому, если Вы используете Eclipse до 4.4 включительно (Luna) и Java 1.8, то null-анализ посредством аннотаций FindBugs работать не будет (bug #435805). В общем, в этом случае лучше переходите на Eclipse 4.5 (Mars).

    Чтение на дом




    IDEA


    IntelliJ IDEA поддерживает null-анализ, кажется, с начала времён (проверки Constant conditions & exceptions и @NotNull/ @Nullable problems).




    Что приятно, среда сразу «из коробки» знает о целом ряде подходящих аннотаций, включая и JSR 305:


    Жаль лишь, что, в отличие от Eclipse, аннотации на пакет целиком (как @NonNullByDefault или @ParametersAreNonnullByDefault) не поддерживаются.

    Обновление


    Начиная со сборки 138.1372 (IDEA-125281), @ParametersAreNonnullByDefault и @ParametersAreNullableByDefault на уровне пакета распознаются и анализируются, хотя и не фигурируют нигде в настройках (спасибо Borz). Более того, функциональность, по-видимому, присутствует и в IDEA 13.1.6 (сборка 135.1306).

    NetBeans


    NetBeans поддерживает null-анализ со времён 7.3, см. след. статьюангл..

    Замечания


    Если Вы используете не только IDE, но и FindBugs, то методы, потенциально возвращающие null, стоит помечать не только как @Nullable, но и как @CheckForNull — FindBugs «успокоится», только если «увидит» @CheckForNull.

    Приятные бонусы ништяки побочные эффекты


    Это лирическое отступление от темы, но оно рассказывает о непосредственной пользе, которую принесло внедрение анализа в проекте X. Точнее, оно помогло выявить горе-«чинителей» модульных тестов.

    Давным-давно были тесты, которые не падали, но были не очень умно написаны. Да, вы не ошиблись, это JUnit 3:

    public void test() throws Exception {
            final Calendar calendar = myVeryCleverNonStandardApiCall();
            final int year = calendar.get(Calendar.YEAR);
            assertEquals(1997, year);
            assertEquals("1.1", System.getProperty("java.specification.version"));
    }
    

    Прошло всего несколько лет, и было замечено, что тесты сломались. Т. е. ещё компилируется, но уже не работает. Тесты были «починены», да так, что комар носа не подточит. Короче говоря, никто ничего не заметил:

    public void test() throws Exception {
            final Calendar calendar = myVeryCleverNonStandardApiCall();
            final int year = calendar.get(Calendar.YEAR);
    //      assertEquals(1997, year);
    //      assertEquals("1.1", System.getProperty("java.specification.version"));
    }
    

    Ещё через несколько лет перестало даже компилироваться, а хорошая статистика была по-прежнему нужна. Тесты были «починены» снова. Поскольку избавиться от NPE было невозможно, а выкидывание большого куска кода привлекло бы внимание, старый тест был замаскирован, а вместо него был введён новый с секундной задержкой для отвода глаз:

    public void test() throws Exception {
            Thread.sleep(1000);
    }
    
    public void _test() throws Exception {
            final Calendar calendar = null; //myVeryCleverNonStandardApiCall();
            final int year = calendar.get(Calendar.YEAR);
    //      assertEquals(1997, year);
    //      assertEquals("1.1", System.getProperty("java.specification.version"));
    }
    

    Вывод?

    Иногда уровень Null pointer access и Potential null pointer access имеет смысл поднимать до «Error» даже для legacy-кода. Особенно для legacy-кода. Вы даже не представляете, на что способны «зубры», создавшие ваш продукт буквально с нуля двадцать лет назад и благополучно ушедшие в какой-нибудь Google или Amazon десять лет назад.

    Пожелания


    Если среди читателей есть люди, использующие C#, прокомментируйте, пожалуйста — актуальна ли проблема для C# в свете наличия такой приятной вещи, как Nullable, и, если да, то какими средствами она решается?
    Share post

    Similar posts

    Comments 26

      +1
      Проблема для C# актуальна, решается точно так же — аннотациями и статическим анализом (причем в том числе и на основе аннотаций JetBrains).
      +1
      Жаль лишь, что, в отличие от Eclipse, аннотации на пакет целиком (как @NonNullByDefault или @ParametersAreNonnullByDefault) не поддерживаются.
      вы уверены? была же доработка и выглядит это теперь так:
      с аннотацией @ParametersAreNonnullByDefault
      с аннотацией @ParametersAreNullableByDefault
        0
        Спасибо, поправил.
          0
          Более того, функциональность, по-видимому, присутствует и в IDEA 13.1.6 (сборка 135.1306)

          если я правильно понял, функциональность эта со сборки 134.738
        +2
        Optional/scala Option задают контракт более явно.
        И не надо никаких обходных маневров. :)
          0
          Да, ладно… Что мешает в метод, принимающий Optional, передать null вместо экземпляра объекта?
            0
            Ревью
              0
              Option защищает от случайных ошибок, а не от целенаправленного головотяпства разработчика, что вполне достаточно. А так да, запретить null вообще было бы более сильным решением, но из-за совместимости с Java приходится это терпеть.
              0
              Option в Scala позволяет коду не падать в таких случаях, но и код корректно не будет работать. По своему опыту скажу, что это намного хуже для поиска ошибки.
                0
                Как правильно сказано в первом комментарии, главное — этот как раз контракт. Если значения может не быть — делаешь Option. Если видишь Option — готовься к тому, что значения может не быть.

                Падать или не падать — вопрос второстепенный. Можно без Option явно проверять все аргументы на null и не падать. Можно наоборот не глядя делать Option.get и падать.
                  0
                  >Падать или не падать — вопрос второстепенный.

                  Вообще-то тут достаточно однозначно, в случае получения как аргумента Option падать нельзя, его нужно обрабатывать. Если нужно падать в случае отсутвия значения, то нужно в момент получения сделать .get, и дальше этот Option не пускать.
                  0
                  Видимо вы столкнулись с неправильным применением Option. Ведь и null ссылки зачастую обрабатываются неверно, тупо все проверяется на null, и в случае null ничего не делается, таким образом происходит проглатывание ошибок.
                • UFO just landed and posted this here
                    0
                    Так далеко ходить не нужно :) Есть c#, kotlin, swift, rust
                    • UFO just landed and posted this here
                  +2
                  У нас тоже аннотации используются — отличная вещь, позволила найти/не допустить несколько ошибок. Плюс это такой зачаток программирования по контракту — когда определяешь метод сразу же аннотируешь возвращаемое значение и входные параметры. И потом проще его писать — не думаешь о том — будет тут null или нет, т.к. всё указано аннотациями плюс IDE проверяет. Ну и использовать этот метод проще — всё помечено аннотациями. Даже бывало укажешь аннотации для метода, а потом смотришь его использование, а там IDE показывает не нужный код — фактически метод возвращает не null, а в местах использования метода проверялось на null и этот код можно сразу удалить :)

                  Ещё пару замечаний по этим аннотациям и IDEA — у нас на разных проектах разные аннотации использовались в качестве NotNull И Nullable и получается, как только импортируешь проект, нужно не забыть указать эти аннотации в IDEA в качестве аннотаций NotNull и Nullable. Что хоть и не долго но не приятно и иногда забываешь. В общем, для проекта с gradle я сделал такую штуку:

                  /*
                  Установить настройки проекта для IDEA. Используется отдельная задача, т.к. задача idea в gradle не поддерживает настройки
                  проекта, который оформлен в виде директорий (так с 14 IDEA работает). Поэтому нужно править xml файл настроек проекта
                  отдельно. 
                   */
                  task setupProject << {
                      //xml файл настроек IDEA
                      File miscXmlFile = new File("./.idea/misc.xml")
                      // Если файла нет, то выходим. Нужно, чтобы не было ошибок в Jenkins, когда не создаются IDEA конфигурации.
                      if (!miscXmlFile.exists()) {
                          return;
                      }
                      XmlParser xmlParser = new XmlParser()
                      Node miscXml = xmlParser.parse(miscXmlFile)
                      // Указываем настройки NotNull и Nullable аннотаций для проекта.
                      Node nullableNotNullManagerNode = miscXml.component.find { it.@name == "NullableNotNullManager" } as Node
                      if (nullableNotNullManagerNode) {
                          miscXml.remove(nullableNotNullManagerNode)
                      }
                      miscXml.append(xmlParser.parseText('''    
                          <component name="NullableNotNullManager"> 
                            <option name="myDefaultNullable" value="com.my_company.npe.Nullable"/>
                            <option name="myDefaultNotNull" value="com.my_company.npe.NotNull"/>
                            <option name="myNullables">
                              <value>
                                <list size="5">
                                  <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable"/>
                                  <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable"/>
                                  <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable"/>
                                  <item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable"/>
                                  <item index="4" class="java.lang.String" itemvalue="com.my_company.npe.Nullable"/>
                                </list>
                              </value>
                            </option>
                            <option name="myNotNulls">
                              <value>
                                <list size="5">
                                  <item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull"/>
                                  <item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull"/>
                                  <item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull"/>
                                  <item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull"/>
                                  <item index="4" class="java.lang.String" itemvalue="com.my_company.npe.NotNull"/>
                                </list>
                              </value>
                            </option>
                          </component>'''
                  
                      ))
                      XmlNodePrinter nodePrinter = new XmlNodePrinter(new PrintWriter(new FileWriter(miscXmlFile)));
                      nodePrinter.preserveWhitespace = true
                      nodePrinter.print(miscXml)
                  }
                  


                  Эта задача позволяет автоматически указывать свои аннотации для проекта. Можно добавить её в задачу «idea», чтобы эти аннотации автоматически конфигурировались при загрузки проекта в IDEA. Пока так не стал делать, т.к. тестировал. При выполнении задачи переоткрывать проект не нужно — IDEA сама подхватит изменения. Плюс я тут вручную xml правлю, т.к. у меня не получить на таком низком уровне работать с конфигами IDEA в gradle.
                    0
                    Кстати, вопрос немного в сторону — я только что узнал про type annotations, которые упоминались в статье ( docs.oracle.com/javase/tutorial/java/annotations/type_annotations.html ) Судя по докам, эти аннотации позволяют проще писать nullable анализ. Правильно? Ну, в смысле, одно из возможностей их использования.
                    И вообще, кто нибудь работал с ними?
                      0
                      При работе с Java 1.8+ авторы JDT рекомендуют использовать для null-анализа именно type annotations, см. Using null type annotations. См. тж. обсуждение bug 392099.
                      +1
                      Аннотации FindBugs не соответствуют этому требованию

                      А патчик можете написать, чтобы поправить это? Если напишете, закиньте нам в багтрекер. Глядишь, успеем в 3.0.1 протолкнуть.
                        0
                        А, или это чисто эклипсовский баг, а не наш?
                          +1
                          Нет, увы.

                          Насколько я понимаю предметную область, это проблема именно FindBugs,
                          но Eclipse 4.5+ уже умеет её обходить.

                          Я постараюсь в ближаёшее время адаптировать аннотации и сообщить Вам.

                          P. S. Спасибо за отклик =)
                            0
                            Вам спасибо за статью. К сожалению, поздно увидел.

                            Насчёт проблемы с полем в Eclipse — проще сразу закинуть поле в локальную переменную, тогда не придётся SuppressWarnings писать:

                            void doSmth() {
                              Object field = this.field;
                              if(field == null) {
                                return;
                              }
                              System.out.println(field.hashCode());
                            }


                            В таком виде Eclipse ругаться не будет. Тут железобетонно не возникнет проблем даже в условиях высокой конкурентности и при выполнении интерпретируемого фрейма (понятно, что JIT-компилятор сделает то же самое — прочитает поле из кучи только один раз). Ну а ещё проще игнорировать варнинг от эклипса и использовать FindBugs, там null-анализ гораздо мощнее :-)

                            Насчёт CheckForNull — нам уже написали. К сожалению, это не моя зона ответственности и высоки шансы, что Билл отклонит это. Я в своих проектах сделал так, как по ссылке — сказал эклипсу считать CheckForNull за Nullable и Nullable не использую вообще.
                        +2
                        Netbeans 8.0.2 не поддерживает @ParametersAreNonnullByDefault

                        Я написал патч, посмотрим как пойдет дело дальше

                        netbeans.org/bugzilla/show_bug.cgi?id=250702
                          +3
                          Буквально сегодня закрыли IDEA-76782: Add compiler support for JSR-305 @Nonnull assertions — теперь еще IDEA сможет вставлять в код NotNull assertions, если включена соответсвующая опция для компилятора, учитывая выбранную конфигурацию, а не только jetbrains NotNull

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