Отказ от ответственности
Во-первых, материал появился потому, что хотелось быстро объяснить
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 работать не будет (Чтение на дом
IDEA
IntelliJ IDEA поддерживает
null
-анализ, кажется, с начала времён (проверки Constant conditions & exceptions и @NotNull/ @Nullable problems).Что приятно, среда сразу «из коробки» знает о целом ряде подходящих аннотаций, включая и JSR 305:
Жаль лишь, что, в отличие от Eclipse, аннотации на пакет целиком (как
@NonNullByDefault
или @ParametersAreNonnullByDefault
) не поддерживаются. Обновление
Начиная со сборки 138.1372 (
@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, и, если да, то какими средствами она решается?