[Из песочницы] Побеждаем 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 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 выглядит подсветка ошибок, найденных инспекциями, в некорректном варианте реализации предыдущего кода: cf2d0be2f50446a19470c343134b80d7.png

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

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

20002e277ae1422695b843b687cb2639.png

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

d22ae46dfb474b21b2b36947120839eb.png

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

f7f6dad327c94eed932fa6da0c0a621f.png

Конец Вот теперь все стало почти замечательно, осталось дождаться выхода Intellij Idea 14. Единственное, чего еще не хватает до полного счастья — это возможности добавления такой метаинформации для внешних библиотек в какой-нибудь external xml. Помнится такую функциональность поддерживали родные аннотации Intellij Idea, правда только для JDK. Ну, и еще нельзя аннотировать тип в Generic без поддержки Type annotations из Java 8. Чего очень не хватает для ListenableFutures и коллекций в редких случаях.Так как объем статьи получился достаточно значительный, то большая часть примеров осталась за бортом, но доступна здесь.

Использованные источники stackoverflow.com/questions/16938241/is-there-a-nonnullbydefault-annotation-in-idea www.jetbrains.com/idea/webhelp/annotating-source-code.html youtrack.jetbrains.com/issue/IDEA-65566 youtrack.jetbrains.com/issue/IDEA-125281

© Habrahabr.ru