JavaでのNullステートメントのチェックを避ける
概要
一般に、null変数、参照、およびコレクションは、Javaコードで処理するのは難しいです。 彼らは識別するのが難しいだけでなく、対処するのも複雑です。
実際のところ、nullを扱う際のミスはコンパイル時に識別できず、実行時にNullPointerExceptionが発生します。このチュートリアルでは、Javaでnullをチェックする必要性と、コード内でnullチェックを回避するのに役立つさまざまな代替案を見ていきます。
Further reading:
Using NullAway to Avoid NullPointerExceptions
Spring Null-Safety Annotations
Introduction to the Null Object Pattern
What Is NullPointerException?
Javadoc for NullPointerExceptionによると、次のようなオブジェクトが必要な場合にアプリケーションがnullを使用しようとするとスローされます。
- nullオブジェクトのインスタスロー可能な値だった
この例外を引き起こすjavaコードのいくつかの例を簡単に見てみましょう:
public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}
ここでは、null参照のメソッド呼び出しを呼び出そうとしました。 これにより、NullPointerExceptionが発生します。別の一般的な例は、null配列にアクセスしようとすると次のようになります。
public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //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をチェックすることです。 しかし、これは多くの冗長なnullチェックを引き起こし、コードを読みにくくします。
次のいくつかのセクションでは、このような冗長性を避けるJavaの代替案のいくつかを見ていきます。
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やその他の開発ツールがこれを開発者にどのように支援するかを見ていきます。
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コントラクトを管理し、静的コード分析ツールの必要性を排除することもできます。 IntelliJ IDEAは、@NonNullおよび@Nullable注釈を提供します。 IntelliJでこれらの注釈のサポートを追加するには、次のMaven依存関係を追加する必要があります:これで、最後の例のように、Nullチェックが欠落している場合、IntelliJは警告を生成します。
IntelliJは、複雑なAPI契約を処理するための契約注釈も提供しています。
5. Assertions
これまでは、クライアントコードからnullチェックの必要性を取り除くことについてのみ説明しました。 しかし、それは現実世界のアプリケーションではめったに適用されません。ここで、nullパラメータを受け入れることができない、またはクライアントが処理する必要があるnull応答を返すことができるAPIで作業しているとします。 これは、null値のパラメータまたは応答をチェックする必要があることを示しています。
ここでは、従来のnullチェック条件文の代わりにJavaアサーションを使用できます。
public void accept(Object param){ assert param != null; doSomething(param);}
2行目では、nullパラメータをチェックします。 アサーションが有効になっている場合、これはAssertionErrorになります。
null以外のパラメータのような事前条件をアサートする良い方法ですが、このアプローチには二つの大きな問題があります:
- アサーションは、通常、JVMで無効になっています
- 偽のアサーションは、回復不可能な未チェックのエラーになります
したがって、プログラマが条件 次のセクションでは、null検証を処理する他の方法について説明します。
コーディング慣行によるNullチェックの回避
6.1. 前提条件
通常、早期に失敗するコードを書くことをお勧めします。 したがって、APIがnullであることが許可されていない複数のパラメータを受け入れる場合は、APIの前提条件としてnull以外のすべてのパラメータをチェッたとえば、早く失敗するメソッドと失敗しないメソッドの2つを見てみましょう。
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()を好むべきです。別の方法として、Apiパラメータを検証するためにGuavaの前提条件を使用することもできます。6.2.
ラッパークラスの代わりにプリミティブを使用する
nullはintのようなプリミティブには許容可能な値ではないので、可能な限りIntegerのようなラッパ
二つの整数を合計するメソッドの二つの実装を考えてみましょう。
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);
nullはintの有効な値ではないため、コンパイル時エラーが発生します。
ラッパークラスで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(); }}
したがって、このメソッドを呼び出す
オブジェクトの使用
Java7では、新しいオブジェクトAPIが導入されました。 このAPIには、多くの冗長なコードを取り除くいくつかの静的ユーティリティメソッドがあります。 このようなメソッドrequireNonNull()を見てみましょう。
public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}
さて、accept()メソッドをテストしましょう。
assertThrows(NullPointerException.class, () -> accept(null));
nullが引数として渡された場合、accept()はNullPointerExceptionをスローします。このクラスには、オブジェクトのnullをチェックする述語として使用できるisNull()メソッドとnonNull()メソッドもあります。
オプションの使用
8.1. OrElseThrow
Java8を使用すると、言語に新しいオプションAPIが導入されました。 これにより、nullと比較してオプションの値を処理するためのより良い契約が提供されます。 Optionalが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; }}
上記のようにOptionalを返すことにより、processメソッドは、応答が空であり、コンパ
これは特に、クライアントコード内のnullチェックの必要性を取り除きます。 空の応答は、オプションのAPIの宣言スタイルを使用して異なる方法で処理できます:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
さらに、API開発者には、APIが空の応答を返すことができることをクライアントに示すためのより良い契約も提供します。
このAPIの呼び出し元に対するnullチェックの必要性はなくなりましたが、空の応答を返すために使用しました。 これを回避するために、Optionalは指定された値を持つOptionalを返すofNullableメソッドを提供します。
public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}
8.2。 空のコレクションを扱うときにOptional with Collections
を使用すると、Optionalが便利です:この関数は、リストの最初の項目を返すことになっています。 Stream APIのfindFirst関数は、データがない場合に空のOptionalを返します。 ここでは、代わりにデフォルト値を提供するためにorElseを使用しました。
これにより、空のリスト、またはStreamライブラリのfilterメソッドを使用した後に提供する項目がないリストのいずれかを処理できます。
または、このメソッドからOptionalを返すことで、クライアントが空の処理方法を決定できるようにすることもできます:
public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}
したがって、getListの結果が空の場合、このメソッドは空のオプションをクライアントに返します。
Optional with collectionsを使用すると、null以外の値を確実に返すApiを設計することができ、クライアントでの明示的なnullチェックを回避できます。ここでは、この実装がnullを返さないgetListに依存していることに注意することが重要です。 しかし、最後のセクションで説明したように、nullではなく空のリストを返す方が良いことがよくあります。8.3.
オプションの組み合わせ
関数がオプションを返すようにするには、その結果を単一の値に結合する方法が必要です。 先ほどのgetListの例を見てみましょう。 Optional listを返す場合、またはOfnullableを使用してOptionalでnullをラップするメソッドでラップする場合はどうなりますか?
私たちのfindFirstメソッドは、オプションのリストのオプションの最初の要素を返したいと思っています。
public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}
getOptionalから返されたOptionalでflatMap関数を使 FlatMapがなければ、結果はOptional<Optional<String>>。 FlatMap操作は、Optionalが空でない場合にのみ実行されます。
ライブラリ
9.1。 Lombok
を使用するLombokは、プロジェクトの定型コードの量を減らす優れたライブラリです。 これには、getter、setter、toString()などのJavaアプリケーションで頻繁に記述するコードの一般的な部分の代わりに、いくつかの注釈が付属しています。その注釈の別のものは@NonNullです。 したがって、プロジェクトがすでに定型コードを排除するためにLombokを使用している場合、@NonNullはnullチェックの必要性を置き換えることができます。
いくつかの例を見る前に、LombokのMaven依存関係を追加しましょう。
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>
ここで、nullチェックが必要な場所で@NonNullを使用できます。
public void accept(@NonNull Object param){ System.out.println(param);}
だから、nullチェックが必要なオブジェクトに注釈を付けるだけで、lombokはコンパイルされたクラスを生成します。p>
public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}
paramがnullの場合、このメソッドはnullpointerexceptionをスローします。 メソッドはコントラクト内でこれを明示的にする必要があり、クライアントコードは例外を処理する必要があります。9.2.
9.2. StringUtils
を使用すると、通常、文字列検証には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.11</version></dependency>
上記のコードをStringUtilsでリファクタリングしてみましょう:そこで、nullまたは空のチェックを静的ユーティリティメソッドisNotEmpty()に置き換えました。 このAPIは、一般的な文字列関数を処理するための他の強力なユーティリティメソッドを提供します。この記事では、NullPointerExceptionのさまざまな理由と、識別が難しい理由を見ていきました。 次に、パラメーター、戻り値の型、およびその他の変数を使用してnullをチェックすることを中心に、コードの冗長性を回避するさまざまな方法を見ました。
すべての例はGitHubで利用可能です。>>>コースをチェックしてください