- PVSM.RU - https://www.pvsm.ru -
Здесь я хочу поделиться используемой мной практикой, которая помогает мне успешно писать почти полностью NPE-free код. Основная ее идея состоит в использовании аннотаций о необязательности значений из библиотеки, реализующей JSR-305 (com.google.code.findbugs: jsr305: 1.3.9):
Естественно обе аннотации применимы к полям объектов и классов, аргументам и возвращаемым значениям методов, локальным переменным. Таким образом эти аннотации дополняют информацию о типе в части обязательности наличия значения.
Но аннотировать все подряд долго и читаемость кода резко снижается. Поэтому, как правило, команда проекта принимает соглашение о том, что все, что не помечено @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 и менее удобного синтаксиса.
Пара слов про наследование и ковариантность/контравариантность:
На самом деле этого уже вполне достаточно и статический анализ (в IDE или на CI) не особо нужен. Но пускай и IDE поработает, не зря же покупали. Я предпочитаю использовать Intellij Idea, поэтому все дальнейшие примеры будут по ней.
Сразу скажу, что по-умолчанию Idea предлагает свои аннотации с аналогичной семантикой, хотя и понимает все остальные. Изменить это можно в Settings -> Inspections -> Probable bugs -> {Constant conditions & exceptions; @NotNull/@Nullable
problems}. В обеих инспекциях нужно выбрать используемую пару аннотаций.
Вот как в Idea выглядит подсветка ошибок, найденных инспекциями, в некорректном варианте реализации предыдущего кода:
Стало совсем замечательно, IDE не только находит два NPE, но и вынуждает нас с ними что-то сделать.
Казалось бы все хорошо, но встроенный статический анализатор Idea не понимает принятого нами соглашения об обязательности по-умолчанию. С ее точки зрения (как и любого другого стат. анализатора) здесь появляется три варианта:
И все что мы не стали размечать теперь считается Unknown. Является ли это проблемой? Для ответа на этот вопрос необходимо понять что же умеют находить инспекции Idea для Nullable и NotNull:
Логично, что любое значение, возвращенное из метода библиотеки, не размеченной данными аннотациями является Unknown. Для борьбы с этим достаточно просто пометить аннотацией локальную переменную или поле, которым осуществляется присваивание.
Если мы продолжаем придерживаться нашей практики, то в нашем коде останется помечено как Nullable все необязательное. Таким образом первая проверка продолжает работать, защищая нас от многих NPE. К сожалению, все остальные проверки отвалились. Не работает в том числе и вторая проверка, крайне полезная против товарищей, очень любящих как писать методы, активно принимающие null в качестве аргументов, так и передавать null в чужие методы, не рассчитанные на это.
Восстановить поведение второй проверки можно двумя способами:
@ParametersAreNonnullByDefault
для задания соответствующего поведения в определенном scope, которым может быть метод, класс, пакет. Это решение уже отлично подходит для legacy проекта. Ложкой дегтя является то, что при задании поведения для пакета рекурсия не поддерживается и на весь модуль за один раз эту аннотацию не навесить.В обоих случаях по-умолчанию NotNull становятся только неаннотированные аргументы методов. Полей, локальных переменных и возвращаемых значений все это не касается.
Улучшить ситуацию призвана грядущая поддержка @TypeQualifierDefault
, которая уже работает в Intellij Idea 14 EAP. С помощью них можно определить свою аннотацию [1] @NonNullByDefault
, которая будет определять обязательность по-умолчанию для всего, поддерживая те же scopes. Рекурсивности сейчас тоже нет, но дебаты идут [2].
Ниже продемонстрировано как выглядят инспекции для трех случаев работы из legacy кода с кодом в новом стиле с аннотациями.
Аннотируем явно:
По-умолчанию только аргументы:
По-умолчанию все:
Вот теперь все стало почти замечательно, осталось дождаться выхода Intellij Idea 14. Единственное, чего еще не хватает до полного счастья — это возможности добавления такой метаинформации для внешних библиотек в какой-нибудь external xml. Помнится такую функциональность поддерживали родные аннотации Intellij Idea, правда только для JDK. Ну, и еще нельзя аннотировать тип в Generic без поддержки Type annotations из Java 8. Чего очень не хватает для ListenableFutures и коллекций в редких случаях.
Так как объем статьи получился достаточно значительный, то большая часть примеров осталась за бортом, но доступна здесь [3].
Автор: tr1cks
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/70149
Ссылки в тексте:
[1] свою аннотацию: http://youtrack.jetbrains.com/issue/IDEA-125281
[2] идут: http://youtrack.jetbrains.com/issue/IDEA-65566
[3] здесь: https://github.com/tr1cks/jsr305-nullability-examples
[4] stackoverflow.com/questions/16938241/is-there-a-nonnullbydefault-annotation-in-idea: http://stackoverflow.com/questions/16938241/is-there-a-nonnullbydefault-annotation-in-idea
[5] www.jetbrains.com/idea/webhelp/annotating-source-code.html: http://www.jetbrains.com/idea/webhelp/annotating-source-code.html
[6] Источник: http://habrahabr.ru/post/237843/
Нажмите здесь для печати.