Перемагаємо 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 — відповідно навпаки.
Природно обидві анотації застосовні до полів об'єктів і класів, аргументів і її обчислене значення методів, локальним змінним. Таким чином ці анотації доповнюють інформацію про типі в частині обов'язковості наявності значення.

Але коментувати все підряд довго і читаність коду різко знижується. Тому, як правило, команда проекту приймає угоду про те, що все, що не позначено
@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 methods for 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 і колекцій в рідкісних випадках.

Так як обсяг статті вийшов досить значний, то велика частина прикладів залишилася за бортом, але доступна тут.

Використані джерела

  1. stackoverflow.com/questions/16938241/is-there-a-nonnullbydefault-annotation-in-idea
  2. www.jetbrains.com/idea/webhelp/annotating-source-code.html
  3. youtrack.jetbrains.com/issue/IDEA-65566
  4. youtrack.jetbrains.com/issue/IDEA-125281

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.