Почему явным образом выбрасываю исключение NullPointerException, а не разрешать это естественным образом?

При чтении исходного кода 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;
}

Ответ 1

Существует ряд причин, которые приходят на ум, некоторые из которых тесно связаны:

Fail-fast: Если он будет терпеть неудачу, лучше провалиться раньше, чем позже. Это позволяет сблизить проблемы с их источником, что облегчает их распознавание и восстановление. Он также позволяет избежать потери циклов процессора в коде, который может быть сбой.

Цель: Бросок исключения явно позволяет разработчикам понять, что ошибка существует намеренно, и автор знал о последствиях.

Согласованность: Если ошибка была разрешена естественным образом, это может не произойти в каждом сценарии. Если сопоставление не найдено, например, remappingFunction никогда не будет использоваться, и исключение не будет выбрано. Предварительное подтверждение ввода позволяет более детерминированное поведение и более четкую документацию.

Стабильность: Код развивается со временем. Код, который встречает исключение, естественно, после небольшого рефакторинга перестает это делать или делает это при разных обстоятельствах. Выброс его явно снижает вероятность непреднамеренного изменения поведения.

Ответ 2

Это для ясности, согласованности и предотвращения выполнения лишней ненужной работы.

Рассмотрим, что произойдет, если в верхней части метода не было предложения охраны. Он всегда вызывал бы hash(key) и getNode(hash, key), даже когда null был передан для remappingFunction до того, как был сброшен NPE.

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

Оба сценария плохие. Если null не является допустимым значением для remappingFunction, метод должен последовательно генерировать исключение независимо от внутреннего состояния объекта во время вызова, и он должен делать это, не делая ненужную работу, которая бессмысленна, учитывая, что это просто собирается бросить. Наконец, это хороший принцип чистого, четкого кода, чтобы иметь защиту прямо перед тем, чтобы любой, кто просматривал исходный код, мог легко видеть, что он это сделает.

Даже если исключение было в настоящее время выбрано каждой ветвью кода, возможно, что изменение в будущем кода изменит это. Выполнение проверки в начале гарантирует, что она будет выполнена.

Ответ 3

В дополнение к причинам, перечисленным в @shmosel, отличный ответ...

Производительность:. Возможно, были/были преимущества производительности (на некоторых JVM), чтобы явно бросать NPE, а не позволять JVM делать это.

Это зависит от стратегии, которую интерпретирует Java-интерпретатор и JIT-компилятор для обнаружения разыменования нулевых указателей. Одна из стратегий состоит в том, чтобы не проверять значение null, а вместо этого захватывать SIGSEGV, который происходит, когда команда пытается получить доступ к адресу 0. Это самый быстрый подход в случае, когда ссылка всегда действительна, но она дорогая в корпусе NPE.

Явный тест для null в коде позволит избежать удара производительности SIGSEGV в сценарии, в котором NPE были частыми.

(Я сомневаюсь, что это была бы целесообразная микро-оптимизация в современной JVM, но это могло быть в прошлом.)


Совместимость: Вероятная причина отсутствия сообщения в исключении - совместимость с NPE, которые выбрасывает сама JVM. В совместимой реализации Java NPE, созданный JVM, имеет сообщение null. (Android Java отличается.)

Ответ 4

Помимо того, что указывают другие люди, стоит отметить роль конвенции здесь. Например, в С# у вас также есть одно и то же соглашение о явном повышении исключения в таких случаях, но в частности это ArgumentNullException, что несколько более конкретно. (Соглашение С# состоит в том, что NullReferenceException всегда представляет собой какую-то ошибку - довольно просто, это никогда не должно происходить в производственном коде; предоставляется, ArgumentNullException обычно тоже, но это может быть ошибкой больше по линии "неправильно вы неправильно понимаете, как правильно использовать библиотеку" ).

Итак, в основном, в С# NullReferenceException означает, что ваша программа действительно пыталась его использовать, тогда как ArgumentNullException означает, что он признал, что значение было неправильным, и даже не пыталось его использовать. Последствия могут быть разными (в зависимости от обстоятельств), потому что ArgumentNullException означает, что данный метод еще не имеет побочных эффектов (поскольку он не прошел предварительные условия метода).

Кстати, если вы поднимаете что-то вроде ArgumentNullException или IllegalArgumentException, эта часть точки проверки: вы хотите другое исключение, чем вы "нормально" получили.

В любом случае, явное повышение исключений подкрепляет хорошую практику того, чтобы быть явным о ваших предпосылках метода и ожидаемых аргументах, что делает код более легким для чтения, использования и поддержки. Если вы не указали явно на null, я не знаю, было ли это потому, что вы считали, что никто никогда не передаст аргумент null, вы рассчитываете, что он все равно выбросит исключение, или вы просто забыли проверьте это.

Ответ 5

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

Ответ 6

Он превращает, казалось бы, неустойчивое условие ошибки в явное нарушение договора: функция имеет некоторые предварительные условия для правильной работы, поэтому она проверяет их заранее, обеспечивая их выполнение.

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

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


Sidenote:
Я не знаю, разрешает ли Java это (я сомневаюсь), но программисты C/С++ используют assert() в этом случае, что значительно лучше для отладки: он сообщает программе сбой немедленно и как можно труднее, если при условии, что условие оценивается как ложное. Итак, если вы запустили

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

    ...
}

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

Ответ 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(), лучше.

Ответ 8

Это намеренно защищать дальнейший ущерб или попасть в противоречивое состояние.