1. 概要

一般に、 null 変数、参照、およびコレクションは、Javaコードで処理するのが難しいです。 それらは特定するのが難しいだけでなく、対処するのも複雑です。

実際のところ、 null の処理に失敗した場合、コンパイル時に識別できず、実行時にNullPointerExceptionが発生します。

このチュートリアルでは、Javaで null をチェックする必要性と、コードでnullチェックを回避するのに役立つさまざまな代替方法について説明します。

2. NullPointerException とは何ですか?

Javadoc for NullPointerException によると、次のようなオブジェクトが必要な場合に、アプリケーションがnullを使用しようとするとスローされます。

  • nullオブジェクトのインスタンスメソッドを呼び出す
  • nullオブジェクトのフィールドへのアクセスまたは変更
  • nullの長さを配列のように取る
  • nullのスロットにアレイであるかのようにアクセスまたは変更する
  • nullThrowable値であるかのようにスローする

この例外の原因となるJavaコードの例をいくつか簡単に見てみましょう。

public void doSomething() {
    String result = doSomethingElse();
    if (result.equalsIgnoreCase("Success")) 
        // success
    }
}

private String doSomethingElse() {
    return null;
}

ここ、 null参照のメソッド呼び出しを呼び出そうとしました。 これにより、NullPointerExceptionが発生します。

もう1つの一般的な例は、null配列にアクセスしようとした場合です。

public static void main(String[] args) {
    findMax(null);
}

private static void findMax(int[] arr) {
    int max = arr[0];
    //check other elements in loop
}

これにより、6行目でNullPointerExceptionが発生します。

したがって、上記の例からわかるように、 null オブジェクトのフィールド、メソッド、またはインデックスにアクセスすると、NullPointerExceptionが発生します。

NullPointerException を回避する一般的な方法は、nullをチェックすることです。

public void doSomething() {
    String result = doSomethingElse();
    if (result != null && result.equalsIgnoreCase("Success")) {
        // success
    }
    else
        // failure
}

private String doSomethingElse() {
    return null;
}

現実の世界では、プログラマーはどのオブジェクトが可能であるかを特定するのが難しいと感じていますヌル。 積極的に安全な戦略は、すべてのオブジェクトのnullをチェックすることです。 ただし、これにより多くの冗長なnullチェックが発生し、コードが読みにくくなります。

次のいくつかのセクションでは、このような冗長性を回避するJavaのいくつかの代替案について説明します。

3. APIコントラクトを介したnullの処理

前のセクションで説明したように、 null オブジェクトのメソッドまたは変数にアクセスすると、NullPointerExceptionが発生します。 また、オブジェクトにアクセスする前にオブジェクトに null チェックを入れると、NullPointerExceptionの可能性がなくなることも説明しました。

ただし、多くの場合、null値を処理できるAPIがあります。

public void print(Object param) {
    System.out.println("Printing " + param);
}

public Object process() throws Exception {
    Object result = doSomething();
    if (result == null) {
        throw new Exception("Processing fail. Got a null response");
    } else {
        return result;
    }
}

print()メソッド呼び出しは、単に「null」を出力しますが、例外をスローしません。 同様に、 process()は、応答でnullを返すことはありません。 むしろ例外をスローします。

したがって、上記のAPIにアクセスするクライアントコードの場合、nullチェックは必要ありません。

ただし、そのようなAPIは、コントラクトで明示的にする必要があります。 APIがそのようなコントラクトを公開するための一般的な場所は、Javadocです。

ただし、これは APIコントラクトの明確な表示を提供しないため、コンプライアンスを確保するためにクライアントコード開発者に依存します。

次のセクションでは、いくつかのIDEやその他の開発ツールが開発者にこれをどのように支援するかを見ていきます。

4. APIコントラクトの自動化

4.1. 静的コード分析の使用

静的コード分析ツールは、コード品質を大幅に向上させるのに役立ちます。 また、そのようなツールのいくつかは、開発者がnullコントラクトを維持することも可能にします。 一例はFindBugsです。

FindBugsは、@ Nullableおよび@NonNullアノテーションを介してnullコントラクトを管理するのに役立ちます。これらのアノテーションは、任意のメソッド、フィールド、ローカル変数、またはパラメーターで使用できます。 これにより、注釈付きタイプがnullであるかどうかがクライアントコードに明示されます。

例を見てみましょう:

public void accept(@NonNull Object param) {
    System.out.println(param.toString());
}

ここで、 @NonNull は、引数をnullにすることはできないことを明確にしています。 クライアントコードが引数のnullをチェックせずにこのメソッドを呼び出すと、FindBugsはコンパイル時に警告を生成します。

4.2. IDEサポートの使用

開発者は通常、Javaコードの記述をIDEに依存しています。 また、スマートコードの完了や、変数が割り当てられていない可能性がある場合などの便利な警告などの機能は、確かに大いに役立ちます。

一部のIDEでは、開発者がAPIコントラクトを管理できるため、静的コード分析ツールが不要になります。 IntelliJIDEAは@NonNullおよび@Nullableアノテーションを提供します。

IntelliJでこれらのアノテーションのサポートを追加するには、次のMaven依存関係を追加する必要があります。

<dependency>
    <groupId>org.jetbrains</groupId>
    <artifactId>annotations</artifactId>
    <version>16.0.2</version>
</dependency>

これで、 IntelliJは、最後の例のように、nullチェックが欠落している場合に警告を生成します。

IntelliJは、複雑なAPIコントラクトを処理するためのContractアノテーションも提供します。

5. アサーション

これまで、クライアントコードからnullチェックの必要性を取り除くことについてのみ話してきました。 しかし、それが実際のアプリケーションに適用されることはめったにありません。

ここで、 nullパラメータを受け入れることができない、またはクライアントが処理する必要のあるnull応答を返すことができるAPIを使用していると仮定します。これにより、パラメータまたは応答を確認する必要があります。 null値の場合。

ここでは、従来のnullチェック条件ステートメントの代わりにJavaアサーションを使用できます。

public void accept(Object param){
    assert param != null;
    doSomething(param);
}

2行目では、nullパラメーターをチェックします。 アサーションが有効になっている場合、 AssertionErrorになります。

null 以外のパラメーターなどの前提条件をアサートするのに適した方法ですが、このアプローチには2つの大きな問題があります

  1. 通常、アサーションはJVMでは無効になっています。
  2. false アサーションは、回復不能なチェックされていないエラーになります。

したがって、プログラマーが条件をチェックするためにアサーションを使用することはお勧めしません。次のセクションでは、null検証を処理する他の方法について説明します。

6. コーディング手法によるNullチェックの回避

6.1. 前提条件

通常、早期に失敗するコードを作成することをお勧めします。 したがって、APIが null にすることを許可されていない複数のパラメーターを受け入れる場合、APIの前提条件としてすべてのnull以外のパラメーターをチェックすることをお勧めします。

2つの方法を見てみましょう。1つは早期に失敗し、もう1つは失敗しません。

public void goodAccept(String one, String two, String three) {
    if (one == null || two == null || three == null) {
        throw new IllegalArgumentException();
    }

    process(one);
    process(two);
    process(three);
}

public void badAccept(String one, String two, String three) {
    if (one == null) {
        throw new IllegalArgumentException();
    } else {
        process(one);
    }

    if (two == null) {
        throw new IllegalArgumentException();
    } else {
        process(two);
    }

    if (three == null) {
        throw new IllegalArgumentException();
    } else {
        process(three);
    }
}

明らかに、 badAccept()よりも goodAccept()を優先する必要があります。

別の方法として、Guavaの前提条件を使用してAPIパラメーターを検証することもできます。

6.2. ラッパークラスの代わりにプリミティブを使用する

null は、 int のようなプリミティブには受け入れられない値であるため、可能な限り、Integerのようなラッパーの対応物よりも優先する必要があります。

2つの整数を合計するメソッドの2つの実装について考えてみます。

public static int primitiveSum(int a, int b) {
    return a + b;
}

public static Integer wrapperSum(Integer a, Integer b) {
    return a + b;
}

次に、クライアントコードでこれらのAPIを呼び出します。

int sum = primitiveSum(null, 2);

nullintの有効な値ではないため、これによりコンパイル時エラーが発生します。

また、ラッパークラスでAPIを使用すると、NullPointerExceptionが発生します。

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

別のチュートリアルJavaプリミティブとオブジェクトで説明したように、ラッパー上でプリミティブを使用する他の要因もあります。

6.3. 空のコレクション

場合によっては、メソッドからの応答としてコレクションを返す必要があります。 このようなメソッドの場合、nullではなく常に空のコレクションを返すようにする必要があります。

public List<String> names() {
    if (userExists()) {
        return Stream.of(readName()).collect(Collectors.toList());
    } else {
        return Collections.emptyList();
    }
}

このようにして、このメソッドを呼び出すときにクライアントがnullチェックを実行する必要がなくなりました。

7. オブジェクトの使用

Java 7では、新しいオブジェクトAPIが導入されました。 このAPIには、多くの冗長コードを取り除くいくつかの静的ユーティリティメソッドがあります。

そのようなメソッドの1つであるrequireNonNull()を見てみましょう。

public void accept(Object param) {
    Objects.requireNonNull(param);
    // doSomething()
}

次に、 accept()メソッドをテストしてみましょう。

assertThrows(NullPointerException.class, () -> accept(null));

したがって、 null が引数として渡された場合、 accept()NullPointerExceptionをスローします。

このクラスには、オブジェクトのnullをチェックするための述語として使用できる isNull()メソッドとnonNull()メソッドもあります。

8. オプションを使用

8.1. またはElseThrowを使用する

Java 8では、この言語で新しいオプションのAPIが導入されました。 これにより、 null と比較して、オプションの値を処理するためのより良いコントラクトが提供されます。

オプションのnullチェックの必要性をどのように取り除くかを見てみましょう。

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);

    if (response == null) {
        return Optional.empty();
    }

    return Optional.of(response);
}

private String doSomething(boolean processed) {
    if (processed) {
        return "passed";
    } else {
        return null;
    }
}

上記のようにオプションのを返すことにより、プロセスメソッドは、応答が空である可能性があり、コンパイル時に処理する必要があることを呼び出し元に明確にします。

これにより、クライアントコードでnullチェックを行う必要がなくなります。 空の応答は、オプションのAPIの宣言型スタイルを使用して別の方法で処理できます。

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

さらに、 API開発者に対して、APIが空の応答を返すことができることをクライアントに示すためのより良い契約を提供します。

このAPIの呼び出し元でnullチェックを行う必要はありませんが、これを使用して空の応答を返しました。

これを回避するために、Optionalは指定された値でOptionalを返すofNullableメソッドを提供します。値がnullの場合は空です

public Optional<Object> process(boolean processed) {
    String response = doSomething(processed);
    return Optional.ofNullable(response);
}

8.2. コレクションでのオプションの使用

空のコレクションを処理する場合、オプションのが便利です。

public String findFirst() {
    return getList().stream()
      .findFirst()
      .orElse(DEFAULT_VALUE);
}

この関数は、リストの最初の項目を返すことになっています。 StreamAPIのfindFirst関数は、データがない場合、空のオプションのを返します。 ここでは、代わりにorElseを使用してデフォルト値を提供しています。

これにより、空のリスト、またはStreamライブラリのfilterメソッドを使用した後、提供するアイテムがないリストのいずれかを処理できます。

または、このメソッドからオプションのを返すことにより、クライアントが空のを処理する方法を決定できるようにすることもできます。

public Optional<String> findOptionalFirst() {
    return getList().stream()
      .findFirst();
}

したがって、 getList の結果が空の場合、このメソッドは空のオプションのをクライアントに返します。

コレクションでオプションのを使用すると、null以外の値を確実に返すAPIを設計できるため、クライアントでの明示的なnullチェックを回避できます。

ここで重要なのは、この実装はに依存しているということです。 getList 戻らないヌル。 ただし、前のセクションで説明したように、多くの場合、 ヌル

8.3. オプションの組み合わせ

関数がOptionalを返すようにするには、それらの結果を1つの値に結合する方法が必要です。

前のgetListの例を見てみましょう。 オプションリストを返す場合、またはofNullableを使用してnullオプションでラップするメソッドでラップする場合はどうなりますか。 ?

findFirst メソッドは、オプションリストのオプション最初の要素を返したいと考えています。

public Optional<String> optionalListFirst() {
   return getOptionalList()
      .flatMap(list -> list.stream().findFirst());
}

getOptionalから返されたOptionalflatMap関数を使用することにより、Optionalを返す内部式の結果を解凍できます。 それなし flatMap 、結果は次のようになりますオプション >> flatMap 操作は、オプションが空でない場合にのみ実行されます。

9. ライブラリ

9.1. ロンボクの使用

Lombok は、プロジェクトのボイラープレートコードの量を減らす優れたライブラリです。 いくつか例を挙げると、getter、setter、 toString()など、Javaアプリケーションで自分で作成することが多いコードの一般的な部分の代わりとなる一連のアノテーションが付属しています。

その注釈のもう1つは、@NonNullです。 したがって、プロジェクトですでにLombokを使用してボイラープレートコードを削除している場合は、@NonNullでnullチェックの必要性を置き換えることができます。

いくつかの例に移る前に、LombokのMaven依存関係を追加しましょう。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>

これで、nullチェックが必要な場所で@NonNullを使用できます。

public void accept(@NonNull Object param){
    System.out.println(param);
}

したがって、 null チェックが必要となるオブジェクトに注釈を付けるだけで、Lombokはコンパイルされたクラスを生成します。

public void accept(@NonNull Object param) {
    if (param == null) {
        throw new NullPointerException("param");
    } else {
        System.out.println(param);
    }
}

paramnullの場合、このメソッドはNullPointerExceptionをスローします。 メソッドはコントラクトでこれを明示的にする必要があり、クライアントコードは例外を処理する必要があります。

9.2. StringUtilsを使用する

通常、 String 検証には、 null 値に加えて、空の値のチェックが含まれます。

したがって、これは一般的な検証ステートメントになります。

public void accept(String param){
    if (null != param && !param.isEmpty())
        System.out.println(param);
}

多くの文字列タイプを処理する必要がある場合、これはすぐに冗長になります。ここで、StringUtilsが便利です。

これが実際に動作するのを見る前に、commons-lang3のMaven依存関係を追加しましょう。

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

上記のコードをStringUtilsでリファクタリングしてみましょう。

public void accept(String param) {
    if (StringUtils.isNotEmpty(param))
        System.out.println(param);
}

そこで、 nullまたは空のチェックを静的ユーティリティメソッドisNotEmpty()に置き換えました。 このAPIは、一般的なString関数を処理するための他の強力なユーティリティメソッドを提供します。

10. 結論

この記事では、 NullPointerException のさまざまな理由と、特定が難しい理由について説明しました。

次に、パラメーター、戻り値の型、その他の変数を使用してnullをチェックする際のコードの冗長性を回避するさまざまな方法を確認しました。

すべての例は、GitHubから入手できます。