Чому явно кидати NullPointerException, а не допускати цього природним шляхом?


182

Читаючи вихідний код JDK, я вважаю, що автор перевіряє параметри, якщо вони є нульовими, а потім викидає нові NullPointerException () вручну. Чому вони це роблять? Я думаю, що цього робити не потрібно, оскільки він викине новий NullPointerException (), коли викликає будь-який метод. (Ось, наприклад, вихідний код HashMap :)

public V computeIfPresent(K key,
                          BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    if (remappingFunction == null)
        throw new NullPointerException();
    Node<K,V> e; V oldValue;
    int hash = hash(key);
    if ((e = getNode(hash, key)) != null &&
        (oldValue = e.value) != null) {
        V v = remappingFunction.apply(key, oldValue);
        if (v != null) {
            e.value = v;
            afterNodeAccess(e);
            return v;
        }
        else
            removeNode(hash, key, null, false, true);
    }
    return null;
}

32
Ключовим моментом кодування є намір
Страшний Вомбат

19
Це дуже гарне запитання для вашого першого запитання! Я вніс кілька незначних змін; Сподіваюся, ви не заперечуєте. Я також видалив подяку та записку про те, що це ваше перше запитання, оскільки зазвичай така штука не є частиною таких питань.
Девід Конрад

11
Я на C # умова полягає в тому, щоб говорити ArgumentNullExceptionу подібних випадках (а не NullReferenceException) - це насправді гарний питання щодо того, чому б ви NullPointerExceptionтут чітко піднімали (а не інший).
EJoshuaS

21
@EJoshuaS Це стара суперечка , слід кидати IllegalArgumentExceptionабо NullPointerExceptionдля нульового аргументу. Конвенція JDK - остання.
шмосель

33
Справжня проблема полягає в тому, що вони кидають помилку і викидають всю інформацію, яка призвела до цієї помилки . Здається, це IS фактичний вихідний код. Навіть не просте криваве рядкове повідомлення. Сумно.
Мартін Ба

Відповіді:


254

Існує ряд причин, які приходять до тями, кілька тісно пов’язаних між собою:

Невдалий: якщо це не вдасться, краще провалитися швидше, ніж пізніше. Це дозволяє виявити проблеми ближче до їх джерела, полегшивши їх ідентифікацію та відновлення. Це також дозволяє уникнути витрачання циклів процесора на код, який повинен вийти з ладу.

Намір: викидання винятку чітко дає зрозуміти керівникам, що помилка є цілеспрямовано, і автор знав про наслідки.

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

Стабільність: Код розвивається з часом. Код, що стикається з винятком, природно може, після невеликого рефакторингу, перестати це робити або зробити це за інших обставин. Кидаючи це явно, стає менш ймовірним випадкове зміни поведінки.


14
Також: таким чином місце, з якого викинуто виняток, прив’язується до точно однієї змінної, яка перевіряється. Без цього виняток може статися через те, що одна з декількох змінних є нульовою.
Єнс Шодер

45
Ще один: якщо ви чекаєте, коли NPE відбудеться природним чином, якийсь код між проміжками вже міг змінити стан вашої програми через побічні ефекти.
Томас

6
Хоча цей фрагмент цього не робить, ви можете використовувати new NullPointerException(message)конструктор, щоб уточнити, що є null. Добре для людей, які не мають доступу до вашого вихідного коду. Вони навіть зробили це однолінійним в JDK 8 Objects.requireNonNull(object, message)корисним методом.
Робін

3
ПЕРЕДБАЧЕННЯ має бути поблизу ПОСТУПНО. "Fail Fast" - це більше, ніж правило. Коли ви не хотіли б такої поведінки? Будь-яка інша поведінка означає, що ви приховуєте помилки. Існують "НЕПРАВНІ" та "ПОМИЛКИ". ЗНАЧЕННЯ - це коли ця програма перетравлює покажчик NULL і збої. Але цей рядок коду не є БЕЗПЕЧНИМ. NULL звідкись прийшов - аргумент методу. Хто передав цей аргумент? З деякого рядка коду, що посилається на локальну змінну. Звідки це ... Бачите? Це смокче. Чия відповідальність повинна була б бачити, як погана цінність зберігається? Ваша програма тоді повинна була збій.
Ной Спур'єр

4
@Thomas Хороший момент. Шмосель: Тома Томаса може мати на увазі невдалий пункт, але він начебто похований. Це досить важлива концепція, що вона має свою власну назву: атомна атомність . Див. Bloch, Ефективна Java , пункт 46. Він має більш сильну семантику, ніж невдалий. Я б запропонував назвати це окремо. Відмінна відповідь загалом, BTW. +1
Стюарт Маркс

40

Це для ясності, послідовності та запобігання виконанню зайвих, непотрібних робіт.

Поміркуйте, що буде, якби у верхній частині методу не було запобіжного застереження. Він завжди буде дзвонити hash(key)і getNode(hash, key)навіть тоді , коли nullбув прийнятий в для remappingFunctionдо NPE був кинутий.

Ще гірше, якщо ifумова така, falseто ми беремо elseгілку, яка взагалі не використовує remappingFunction, а це означає, що метод не завжди кидає NPE при nullпередачі a ; чи залежить це, залежить від стану карти.

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

Навіть якби виняток зараз кидав кожну гілку коду, можливо, майбутня редакція коду змінила б це. Виконання перевірки на початку гарантує, що вона обов'язково буде виконана.


25

Окрім причин, перелічених чудовою відповіддю @ shmosel ...

Продуктивність: Можливо, існували / були переваги від ефективності (на деяких JVM), щоб явно кидати NPE, а не дозволяти JVM робити це.

Це залежить від стратегії, яку застосовують інтерпретатор Java та компілятор JIT для виявлення перенаправлення нульових покажчиків. Однією з стратегій є не тестування на null, а замість цього захопити SIGSEGV, що відбувається, коли інструкція намагається отримати доступ до адреси 0. Це найшвидший підхід у тому випадку, коли посилання завжди дійсне, але воно є дорогим у випадку NPE.

Явний тест nullв коді дозволить уникнути досягнення ефективності SIGSEGV у сценарії, коли NPE були частими.

(Сумніваюсь, що це було б вагомою мікрооптимізацією в сучасному JVM, але це могло бути і раніше.)


Сумісність: ймовірною причиною відсутності повідомлення про виняток є сумісність з NPE, які викидає сам JVM. У сумісному виконанні Java, NPE, кинутий JVM, містить nullповідомлення. (Android Java відрізняється.)


20

Окрім того, що вказали інші, тут варто відзначити роль конвенції. Наприклад, у C # ви також маєте таку ж угоду про явний збір виключення у подібних випадках, але це конкретно таке ArgumentNullException, яке дещо конкретніше. (C # конвенції є те , що NullReferenceException завжди є помилку в який - то - досить просто, він не повинен коли - або станеться у виробництві код, надається, як ArgumentNullExceptionправило , робить теж, але це може бути помилка більше уздовж лінії «ви не зрозуміти, як правильно використовувати бібліотеку «вид помилки».

Так, в основному, в C # NullReferenceExceptionозначає, що ваша програма насправді намагалася ним користуватися, тоді як ArgumentNullExceptionце означає, що вона визнала, що значення було неправильним, і навіть не турбувало його намагатися використовувати. Наслідки можуть насправді бути різними (залежно від обставин), оскільки ArgumentNullExceptionозначає, що розглянутий метод ще не мав побічних ефектів (оскільки він не зумів передумов методу).

Між іншим, якщо ви піднімаєте щось на кшталт ArgumentNullExceptionабо IllegalArgumentException, це є частиною сенсу робити перевірку: ви хочете, щоб виняток був іншим, ніж ви зазвичай отримаєте.

Так чи інакше, явне підвищення винятку підсилює добру практику бути чіткими щодо попередніх умов та очікуваних аргументів вашого методу, що полегшує читання, використання та підтримку коду. Якщо ви явно не перевіряли null, я не знаю, чи це тому, що ви думали, що ніхто ніколи не передасть nullаргумент, ви вважаєте, що все-таки кинете виняток, або просто забули це перевірити.


4
+1 для середнього абзацу. Я заперечую, що розглянутий код повинен "викинути новий IllegalArgumentException (" перезавантаженняFunction не може бути нульовим ")." Таким чином, відразу видно, що було не так. Показані NPE дещо неоднозначні.
Кріс Паркер

1
@ChrisParker Я був такої ж думки, але виявляється, що NullPointerException призначений для позначення нульового аргументу, переданого методу, що очікує ненульового аргументу, на додаток до відповіді часу на спробу зняття нуля. Від javadoc: "Програми повинні викидати екземпляри цього класу, щоб вказати на інші незаконні використання nullоб'єкта." Я не збожеволів у цьому, але, здається, це задумана конструкція.
VGR

1
Я погоджуюся, @ChrisParker - я вважаю, що цей виняток є більш конкретним (оскільки код навіть не намагався зробити щось зі значенням, він одразу визнав, що не повинен його використовувати). Мені подобається умова C # у цьому випадку. Конвенція C # полягає в тому, що NullReferenceException(еквівалентно NullPointerException) означає, що ваш код насправді намагався використовувати його (що завжди є помилкою - це ніколи не повинно траплятися у виробничому коді). спробуйте його використати ". Є також ArgumentException(що означає, що аргумент був неправильним з якоїсь іншої причини).
EJoshuaS

2
Я скажу це дуже багато, Я ВИНАГА кидаю незаконний аргументЕксцепція, як описано. Я завжди відчуваю себе комфортно плавучою конвенцією, коли відчуваю, що конвенція німа.
Кріс Паркер

1
@PieterGeerkens - так, бо рядок 35 NullPointerException так набагато кращий, ніж IllegalArgumentException ("Функція не може бути нульовою"). 35 Серйозно?
Кріс Паркер

12

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


9

Це перетворює, здавалося б, помилкову умову помилки в явне порушення договору: функція має деякі передумови для коректної роботи, тому вона попередньо перевіряє їх, змушуючи їх виконувати.

Ефект полягає в тому, що вам не доведеться налагоджувати, computeIfPresent()коли ви отримуєте з нього виключення. Як тільки ви побачите, що виняток відбувається з перевірки попереднього стану, ви знаєте, що ви викликали функцію з незаконним аргументом. Якщо перевірки там не було, вам слід виключити можливість того, що в computeIfPresent()собі є якась помилка , яка призводить до того, що виняток буде кинуто.

Очевидно, що кидати загальне NullPointerException- це дійсно поганий вибір, оскільки це не є сигналом про порушення договору саме по собі. IllegalArgumentExceptionбув би кращим вибором.


Sidenote:
Я не знаю, чи дозволяє Java це (я сумніваюся в цьому), але програмісти C / C ++ використовують assert()у цьому випадку, що значно краще для налагодження: Це говорить про те, що програма повинна вийти з ладу негайно та як можна сильніше, якщо надається умова оцінюють до хибної. Отже, якщо ти побігла

void MyClass_foo(MyClass* me, int (*someFunction)(int)) {
    assert(me);
    assert(someFunction);

    ...
}

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


1
assert something != null;але -assertionsдля запуску програми потрібен прапор. Якщо -assertionsпрапора немає, ключове слово assrt не кине AssertionException
Zoe

Я згоден, тому я віддаю перевагу # угоду C тут - посилання на нуль, неприпустимий аргумент, і нульовий аргумент все зазвичай мають на увазі помилку який - то, але вони мають на увазі різні види помилок. "Ви намагаєтесь використовувати нульову посилання", часто відрізняється від "ви неправильно використовуєте бібліотеку".
EJoshuaS

7

Це тому, що можливо, щоб це не сталося природним шляхом. Давайте подивимося фрагмент коду так:

bool isUserAMoron(User user) {
    Connection c = UnstableDatabase.getConnection();
    if (user.name == "Moron") { 
      // In this case we don't need to connect to DB
      return true;
    } else {
      return c.makeMoronishCheck(user.id);
    }
}

(звичайно, у цьому зразку є чимало проблем щодо якості коду. Вибачте, ліниво уявляю ідеальний зразок)

Ситуація, коли cнасправді не буде використано і NullPointerExceptionне буде кинуто, навіть якщо c == nullце можливо.

У складніших ситуаціях полювати на такі випадки стає дуже непросто. Ось чому загальна перевірка як if (c == null) throw new NullPointerException()краще.


Можливо, шматок коду, який працює без підключення до бази даних, коли він насправді не потрібен, - це добре, а код, який підключається до бази даних просто для того, щоб побачити, чи не може це зробити, зазвичай досить дратує.
Дмитро Григор’єв

5

Навмисно захистити подальше пошкодження або потрапити в непослідовний стан.


1

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

Ви можете додати повідомлення, якщо створили власний виняток

Якщо ви кинете своє, NullPointerExceptionможете додати повідомлення (яке ви обов'язково повинні!)

Повідомлення за замовчуванням є nullвід new NullPointerException()і все методи , які використовують його, наприклад Objects.requireNonNull. Якщо ви надрукуєте цю нуль, вона може навіть перетворитися на порожній рядок ...

Трохи короткий і неінформативний ...

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

Тепер уявіть, що NPE загортається та надсилається через мережу, наприклад, як повідомлення про помилку веб-служби, можливо, між різними відділами чи навіть організаціями. Найгірший сценарій, ніхто не може з'ясувати, що nullозначає "...

Виклики методу ланцюга дозволять вам здогадуватися

Виняток повідомляє лише про те, в якому рядку стався виняток. Розглянемо наступний рядок:

repository.getService(someObject.someMethod());

Якщо ви отримаєте NPE і він вказує на цей рядок, який із них був repositoryі someObjectнедійсним?

Натомість перевірка цих змінних, коли ви їх отримаєте, принаймні вказуватиме на рядок, де вони, сподіваємось, єдина змінна, що обробляється. І, як було сказано раніше, ще краще, якщо ваше повідомлення про помилку містить ім’я змінної чи подібної.

Помилки при обробці великої кількості вхідних даних повинні давати ідентифікаційну інформацію

Уявіть, що ваша програма обробляє вхідний файл тисячами рядків і раптом з’являється NullPointerException. Ви дивитесь на місце і розумієте, що введення було неправильним ... який вхід? Вам знадобиться додаткова інформація про номер рядка, можливо, стовпець або навіть весь текст рядка, щоб зрозуміти, який рядок у цьому файлі потребує виправлення.

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