Pull to refresh

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

Reading time5 min
Views22K

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


Во-первых, материал появился потому, что хотелось быстро объяснить индусам коллегам, что такое 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, и, если да, то какими средствами она решается?
Tags:
Hubs:
+12
Comments26

Articles