web-dev-qa-db-ja.com

自然に発生させるのではなく、明示的にNullPointerExceptionをスローするのはなぜですか?

JDKソースコードを読むとき、著者がパラメータがnullの場合にパラメータをチェックし、新しい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;
}
180
LiJiaming

頭に浮かぶいくつかの理由があり、いくつかは密接に関連しています:

Fail-fast:失敗する場合は、後よりも早く失敗することをお勧めします。これにより、問題をその発生源に近づけることができるため、問題の特定と回復が容易になります。また、失敗するはずのコードでCPUサイクルを浪費することも避けます。

意図:例外を明示的にスローすることで、エラーが意図的に存在し、作者が結果を認識していることをメンテナーに明確に示します。

一貫性:エラーが自然に発生することが許可されている場合、すべてのシナリオで発生するわけではありません。たとえば、マッピングが見つからない場合、remappingFunctionは使用されず、例外はスローされません。事前に入力を検証することで、より確定的な動作とより明確な ドキュメント が可能になります。

安定性:コードは時間とともに進化します。例外に遭遇したコードは、少しのリファクタリングの後、自然にそうするのをやめるか、異なる状況下でそうするかもしれません。明示的にスローすると、動作が誤って変更される可能性が低くなります。

249
shmosel

これは、明確性、一貫性、および余分な不要な作業の実行を防ぐためです。

メソッドの上部にガード句がなかったらどうなるかを考えてください。 NPEがスローされる前にnullname__にremappingFunctionname__が渡された場合でも、常にhash(key)およびgetNode(hash, key)を呼び出します。

さらに悪いことに、ifname__条件がfalsename__の場合、elsename__ブランチを使用します。これはremappingFunctionname__をまったく使用しません。つまり、nullname__が渡されたときにメソッドがNPEをスローしない場合実行されるかどうかは、マップの状態によって異なります。

どちらのシナリオも悪いです。 nullname__がremappingFunctionname__の有効な値ではない場合、メソッドは呼び出し時のオブジェクトの内部状態に関係なく一貫して例外をスローする必要があります。スロー。最後に、ソースコードをレビューする人がすぐにそれができることをすぐにわかるように、ガードを前もって用意することは、クリーンで明確なコードの良い原則です。

例外がコードのすべてのブランチによって現在スローされていたとしても、コードの将来のリビジョンがそれを変更する可能性があります。最初にチェックを実行すると、確実に実行されます。

39
David Conrad

@shmoselの優れた回答に挙げられている理由に加えて...

パフォーマンス: JVMに任せるのではなく、明示的にNPEをスローするほうが(一部のJVMで)パフォーマンス上のメリットがある場合があります.

これは、JavaインタープリターおよびJITコンパイラーがNULLポインターの逆参照を検出するための戦略に依存します。 1つの戦略は、nullをテストせず、代わりに命令がアドレス0にアクセスしようとしたときに発生するSIGSEGVをトラップすることです。これは、参照が常に有効な場合の最速のアプローチですが、高価です= NPEの場合。

コード内のnullの明示的なテストは、NPEが頻繁に発生するシナリオでのSIGSEGVパフォーマンスヒットを回避します。

(これは現代のJVMで価値のあるマイクロ最適化になるとは思いませんが、過去にあったかもしれません。)


互換性:例外にメッセージがないという考えられる理由は、JVM自体によってスローされるNPEとの互換性のためです。準拠のJava実装では、JVMによってスローされたNPEにはnullメッセージがあります。 (Android Javaは異なります。)

25
Stephen C

他の人が指摘したこととは別に、ここでの慣習の役割に​​注目する価値があります。たとえば、C#では、このような場合に明示的に例外を発生させるという同じ規則もありますが、具体的にはArgumentNullExceptionであり、これはやや具体的です。 (C#の規則では、NullReferenceExceptionalwaysはある種のバグを表します-単純に、実稼働コードではeverは発生しません;もちろん、ArgumentNullExceptionもそうです、しかし、「ライブラリを正しく使用する方法がわからない」というバグに沿ったバグかもしれません)。

つまり、基本的に、C#のNullReferenceExceptionは、プログラムが実際に使用しようとしたことを意味しますが、ArgumentNullExceptionは、値が間違っていることを認識し、使用しようとさえしなかったことを意味します。 ArgumentNullExceptionは、(メソッドの前提条件に失敗したため)問題のメソッドにまだ副作用がなかったことを意味するため、実際には意味が異なる場合があります(状況によって異なります)。

ちなみに、ArgumentNullExceptionIllegalArgumentExceptionのようなものを生成する場合、それはチェックを行うポイントの一部です。「通常」取得するのとは異なる例外が必要です。

いずれにせよ、明示的に例外を発生させることで、メソッドの前提条件と予想される引数を明示的にすることで、コードの読み取り、使用、および保守が容易になります。 nullを明示的にチェックしなかった場合、誰もnull引数を渡さないと考えたためか、とにかく例外をスローするためにカウントしているのか、それともチェックするのを忘れたのか、わかりません。

20
EJoshuaS

そのため、エラーを実行するとすぐに例外が発生しますが、後でマップを使用しているときにエラーが発生した理由がわからなくなります。

12
user207421

一見不規則なエラー状態を明確な契約違反に変えます。関数には正しく動作するためのいくつかの前提条件があるため、事前にそれらをチェックし、満たすことを強制します。

その結果、例外を取得するときにcomputeIfPresent()をデバッグする必要がなくなります。例外が前提条件チェックによるものであることがわかると、不正な引数を使用して関数を呼び出したことがわかります。チェックがなかった場合、例外がスローされる原因となるcomputeIfPresent()自体に何らかのバグがある可能性を除外する必要があります。

明らかに、ジェネリックNullPointerExceptionをスローすることは、それ自体で契約違反を示すものではないため、本当に悪い選択です。 IllegalArgumentExceptionの方が適しています。


サイドノート:
Javaがこれを許可するかどうかはわかりませんが(疑いがあります)、C/C++プログラマーはこの場合assert()を使用します。これはデバッグに非常に優れています。指定された条件がfalseと評価された場合、できるだけ早くクラッシュします。だから、走ったら

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

    ...
}

デバッガーの下で、何かがNULLをいずれかの引数に渡した場合、プログラムはどの引数がNULLであるかを伝える行で停止し、呼び出しスタック全体のすべてのローカル変数をゆっくりと調べることができます。

9
cmaster

それは、notが自然に発生する可能性があるからです。次のようなコードを見てみましょう。

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が実際に使用されず、c == nullが可能であってもNullPointerExceptionがスローされない状況。

より複雑な状況では、そのような場合を追い詰めることは非常に簡単ではなくなります。これが、if (c == null) throw new NullPointerException()のような一般的なチェックが優れている理由です。

7
Arenim

それ以上の損傷を保護したり、一貫性のない状態になることを意図しています。

5
Fairoz