Чому варто використовувати Objects.requireNonNull ()?


214

Я зазначив, що багато методів Java 8 в Oracle JDK використовують Objects.requireNonNull(), який внутрішньо передає, NullPointerExceptionякщо даний об'єкт (аргумент) є null.

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

Але NullPointerExceptionбуде кинуто в будь-якому випадку, якщо nullоб’єкт буде відмежований. Отже, чому слід робити цю додаткову нульову перевірку і кидати NullPointerException?

Одним із очевидних відповідей (або користі) є те, що це робить код читабельнішим, і я згоден. Мені цікаво дізнатися будь-які інші причини використання Objects.requireNonNull()на початку методу.




1
Такий підхід до перевірки аргументів дозволяє вам обманювати, коли ви пишете одиничні тести. Spring також має подібні утиліти (див. Docs.spring.io/spring/docs/current/javadoc-api/org/… ). Якщо у вас є "якщо" і хочете мати високий рівень охоплення тестом, ви повинні охопити обидві гілки: коли умова виконана і коли умова не виконана. Якщо ви користуєтесь Objects.requireNonNull, ваш код не має розгалуження, тож за один пропуск одиничного тесту ви отримаєте 100% охоплення :-)
xorcus

Відповіді:


214

Тому що ви можете зробити це явним , зробивши це. Подібно до:

public class Foo {
  private final Bar bar;

  public Foo(Bar bar) {
    Objects.requireNonNull(bar, "bar must not be null");
    this.bar = bar;
  }

Або коротше:

  this.bar = Objects.requireNonNull(bar, "bar must not be null");

Тепер ви знаєте :

  • коли об'єкт Foo був успішно створений за допомогоюnew()
  • тоді гарантується, що його барне поле буде недійсним.

Порівняйте це з: ви створюєте об'єкт Foo сьогодні, а завтра ви викликаєте метод, який використовує це поле і кидає. Швидше за все, ви не знаєте завтра, чому це посилання було вчора недійсним, коли воно було передане конструктору!

Іншими словами: явно використовуючи цей метод для перевірки вхідних посилань, ви можете контролювати момент часу, коли буде викинуто виняток. І більшу частину часу ти хочеш провалитися якомога швидше !

Основними перевагами є:

  • як сказано, контрольована поведінка
  • простіша налагодження - тому що ви кидаєтеся в контекст створення об'єкта. У той момент, коли у вас є певний шанс, що ваші журнали / сліди підкажуть, що пішло не так!
  • і як показано вище: справжня сила цієї ідеї розгортається в поєднанні з кінцевими полями. Тому що тепер будь-який інший код у вашому класі може сміливо вважати, що barвін не є нульовим - і, таким чином, вам не потрібні if (bar == null)перевірки в інших місцях!

23
Ви можете зробити свій код більш компактним, написавшиthis.bar = Objects.requireNonNull(bar, "bar must not be null");
Кіріл Рахман

3
@KirillRakhman Навчання GhostCat чогось крутого, про який я не знав -> виграш квитка в лотереї GhostCat за виграш.
GhostCat

1
Я думаю, що коментар №1 тут є фактичною перевагою Objects.requireNonNull(bar, "bar must not be null");. Дякую за це.
Каву

1
Який із них був першим, і чому люди, на "мові", слідкують за авторами якоїсь бібліотеки ?! Чому гуава не слідкує за поведінкою Java Янг ?!
GhostCat

1
У this.bar = Objects.requireNonNull(bar, "bar must not be null");конструкторах це нормально, але потенційно небезпечно в інших методах, якщо в одному і тому ж методі встановлено дві чи більше змінних. Приклад: talkwards.com/2018/11/03/…
Ерк

100

Невдалий

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

Це зазвичай називають "невдалий ранній" або "невдалий" .


40

Але NullPointerException все одно буде викинуто, якщо нульовий об'єкт буде відпущений. Отже, чому слід робити цю додаткову нульову перевірку і кидати NullPointerException?

Це означає, що ви виявите проблему негайно та надійно .

Поміркуйте:

  • Посилання може бути використане не пізніше в методі, після того, як ваш код вже виконав деякі побічні ефекти
  • Посилання може взагалі не бути відхиленим у цьому методі
    • Він може бути переданий абсолютно іншому коду (тобто причина та помилка далеко не однакові в кодовому просторі)
    • Він може бути використаний набагато пізніше (тобто причини та помилки значно відстають у часі)
  • Десь може бути використано, що нульова посилання є дійсною, але має ненавмисний ефект

.NET робить це краще, відокремлюючи NullReferenceException("ви відмінили нульове значення") від ArgumentNullException("ви не повинні були передавати null як аргумент - і це було для цього параметра). Я хотів би, щоб Java зробила те ж саме, але навіть з просто a NullPointerException, все-таки набагато простіше виправити код, якщо помилка передається в першій точці, в якій її можна виявити.


12

Використання requireNonNull()в якості перших висловлювань у методі дозволяє ідентифікувати зараз / швидку причину винятку.
Трафік чітко вказує на те, що виняток було кинуто, як тільки було введено метод, оскільки абонент не дотримувався вимог / контракту. Передача nullоб'єкта іншому методу дійсно може спровокувати виняток за раз, але причину проблеми може бути складніше зрозуміти, оскільки виняток буде кинутий у конкретному виклику nullоб'єкта, який може бути набагато далі.


Ось конкретний і реальний приклад, який показує, чому ми маємо віддавати перевагу швидкому Object.requireNonNull()виходу з ладу, і, особливо, використовуючи чи будь-який спосіб здійснити недійсну перевірку параметрів, розроблених як не null.

Припустимо, що Dictionaryклас , який складає LookupServiceі Listз Stringпредставляють слова , що містяться в. Ці поля призначені для НЕ nullі один з них передається в Dictionary конструктор.

Тепер припустимо, що "погана" реалізація Dictionaryбез nullперевірки у записі методу (ось такий конструктор):

public class Dictionary {

    private final List<String> words;
    private final LookupService lookupService;

    public Dictionary(List<String> words) {
        this.words = this.words;
        this.lookupService = new LookupService(words);
    }

    public boolean isFirstElement(String userData) {
        return lookupService.isFirstElement(userData);
    }        
}


public class LookupService {

    List<String> words;

    public LookupService(List<String> words) {
        this.words = words;
    }

    public boolean isFirstElement(String userData) {
        return words.get(0).contains(userData);
    }
}

Тепер давайте звернемося до Dictionaryконструктора з nullпосиланням на wordsпараметр:

Dictionary dictionary = new Dictionary(null); 

// exception thrown lately : only in the next statement
boolean isFirstElement = dictionary.isFirstElement("anyThing");

JVM кидає NPE на цю заяву:

return words.get(0).contains(userData); 
Виняток у потоці "main" java.lang.NullPointerException
    на LookupService.isFirstElement (LookupService.javabilje)
    у Dictionary.isFirstElement (Словник.java:15)
    у Dictionary.main (Dictionary.java:22)

Виняток спрацьовує в LookupServiceкласі, тоді як походження його набагато раніше ( Dictionaryконструктор). Це робить загальний аналіз проблеми набагато менш очевидним.
Є words null? Є words.get(0) null? Обидва? Чому один, другий чи, можливо, обидва null? Це помилка кодування в Dictionary(конструктор? Викликаний метод?)? Це помилка кодування в LookupService? (конструктор? викликаний метод?)?
Нарешті, нам доведеться перевірити більше коду, щоб знайти джерело помилок, а в більш складному класі можливо навіть використовувати відладчик, щоб легше зрозуміти, що це сталося.
Але чому проста справа (відсутність нульової перевірки) стає складною проблемою?
Тому що ми дозволили початкову помилку / відсутність ідентифікувати за певним витоком компонентів для нижчих компонентів.
Уявіть, що це LookupServiceбула не локальна служба, а віддалений сервіс або бібліотека сторонньої сторони з малою інформацією про налагодження, або уявіть, що до цього не nullбуло виявлено 2 шари, а 4 або 5 шарів викликів об’єктів ? Проблему було б ще складніше проаналізувати.

Тож спосіб вигоди:

public Dictionary(List<String> words) {
    this.words = Objects.requireNonNull(words);
    this.lookupService = new LookupService(words);
}

Таким чином, немає головного болю: ми виключаємо випадок, як тільки це буде отримано:

// exception thrown early : in the constructor 
Dictionary dictionary = new Dictionary(null);

// we never arrive here
boolean isFirstElement = dictionary.isFirstElement("anyThing");
Виняток у потоці "main" java.lang.NullPointerException
    на java.util.Objects.requireNonNull (Objects.java:203)
    у com.D Dictionary. (Словник.java:15)
    на com.Dictionary.main (Dictionary.java:24)

Зауважте, що тут я проілюстрував проблему з конструктором, але виклик методу міг би мати те саме обмеження, що не стосується нуля.


Оце Так! Чудове пояснення.
Джонатас Насіменто

2

Як зауваження, ця невдача раніше Object#requireNotNullбула реалізована дещо інакше, перш ніж java-9 всередині деяких класів jre. Припустимо, випадок:

 Consumer<String> consumer = System.out::println;

У java-8 це складається як (лише відповідні частини)

getstatic Field java/lang/System.out
invokevirtual java/lang/Object.getClass

В основному це операція як: yourReference.getClass- яка не вдасться, якщо ваша Референція є null.

Все змінилося в jdk-9, де складається той самий код, що і

getstatic Field java/lang/System.out
invokestatic java/util/Objects.requireNonNull

Або в основному Objects.requireNotNull (yourReference)


1

Основне використання - це перевірка та кидання NullPointerExceptionнегайно.

Одна кращою альтернативою (ярлик) для задоволення тих же вимог є @NonNull анотації на Ломбках.


1
Анотація не є заміною методу "Об'єкти", вони працюють разом. Ви не можете сказати @ NonNull x = mightBeNull, ви б сказали @ NonNull x = Objects.requireNonNull (mightBeNull, "немислиме!");
Білл К

@BillK Вибачте, я не отримую вас
Supun Wijerathne

Я просто говорив, що анотація Nonnull працює з RequNonNull, це не альтернатива, але вони працюють разом досить добре.
Білл К

реалізувати невдалу швидку природу, її альтернативне право? Так, я погоджуюсь, це не є альтернативою цього завдання.
Supun Wijerathne

Я думаю, я б назвав RequNonNull () "конверсією" від @ Nullable до @ NonNull. Якщо ви не використовуєте анотації, метод насправді не дуже цікавий (оскільки все, що він робить, це кинути NPE так само, як і код, який він захищає) - хоча він досить чітко показує ваші наміри.
Білл К

1

Виняток нульового вказівника видається, коли ви отримуєте доступ до члена об'єкта, який знаходиться nullв більш пізньому пункті. Objects.requireNonNull()негайно перевіряє значення і миттєво кидає виняток, не рухаючись вперед.


0

Я думаю, що його слід використовувати в конструкторах копій та деяких інших випадках, таких як DI, вхідний параметр якого є об'єктом, ви повинні перевірити, чи параметр недійсний. За таких обставин ви можете зручно використовувати цей статичний метод.


0

У контексті розширень компілятора, які реалізують перевірку Nullability (наприклад: uber / NullAway ), Objects.requireNonNullслід використовувати дещо щадно для випадків, коли у вас є поле, яке може бути зведене нанівець, і, напевно , знаєте, що воно не є нульовим у певний момент вашого коду.

Таким чином, існує два основних звичаї:

  • Перевірка

    • тут уже охоплені інші відповіді
    • перевірка виконання з накладними та потенційними NPE
  • Позначення нестабільності (змінюється від @Nullable до @Nonnull)

    • мінімальне використання перевірки виконання на користь перевірки часу компіляції
    • працює лише тоді, коли примітки правильні (примусові компілятором)

Приклад використання маркування Nullability:

@Nullable
Foo getFoo(boolean getNull) { return getNull ? null : new Foo(); }

// Changes contract from Nullable to Nonnull without compiler error
@Nonnull Foo myFoo = Objects.requireNonNull(getFoo(false));

0

Окрім усіх правильних відповідей:

Ми використовуємо його в реактивних потоках. Зазвичай отримані NullpointerExceptions вкладаються в інші винятки залежно від їх появи в потоці. Отже, згодом ми можемо легко вирішити, як поправити помилку.

Просто приклад: Уявіть, що у вас є

<T> T parseAndValidate(String payload) throws ParsingException { ... };
<T> T save(T t) throws DBAccessException { ... };

де parseAndValidateобгортання NullPointerExceptionз requireNonNullв а ParsingException.

Тепер ви можете вирішити, наприклад, коли робити повторний чи ні:

...
.map(this::parseAndValidate)
.map(this::save)
.retry(Retry.<T>allBut(ParsingException.class))

Без перевірки в методі виникне NullPointerException save, що призведе до нескінченних спроб. Навіть варто, уявіть собі довгоживучу підписку, додавши

.onErrorContinue(
    throwable -> throwable.getClass().equals(ParsingException.class),
    parsingExceptionConsumer()
)

Тепер RetryExhaustExceptionзагине ваша підписка.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.