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 — соответственно наоборот.
Естественно обе аннотации применимы к полям объектов и классов, аргументам и возвращаемым значениям методов, локальным переменным. Таким образом эти аннотации дополняют информацию о типе в части обязательности наличия значения.
Но аннотировать все подряд долго и читаемость кода резко снижается. Поэтому, как правило, команда проекта принимает соглашение о том, что все, что не помечено @Notnull
, является обязательным. С этой практикой хорошо знакомы те, кто использовал 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. Единственное, чего еще не хватает до полного счастья — это возможности добавления такой метаинформации для внешних библиотек в какой-нибудь 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
Автор: tr1cks