免責事項
- この記事はアメリカの発見のふりをせず、一般化され抽象化された性格を持っています。 コード内でNPEを処理する方法は決して新しいものではありませんが、私たちが望んでいるほど知られていません。
- 1回限りのNPEは、おそらくすべての可能なエラーの中で最も単純です。 これはまさに、処理のポリシーがないためにNPEが支配的な状況です。
- この記事では、Java 6および7(MayBeモナド、JSR-308、および型注釈)に適用できないアプローチについては説明しません。
- ユビキタス防御プログラミングは、コードを大量に散らかし、パフォーマンスを低下させ、その結果、依然として望ましい効果が得られないため、闘争の方法とは見なされません。
- 使用されている用語と一般に受け入れられている用語との間に矛盾がある場合があります。 また、Intellij Ideaが使用するチェックの説明は、ソースコードからではなく、ドキュメントおよび観察された動作から取得されるため、完全かつ正確であると主張していません。
JSR-305による救助
ここでは、ほぼ完全にNPEフリーのコードを正常に作成するのに役立つ、使用しているプラクティスを共有したいと思います。 その主なアイデアは、JSR-305(com.google.code.findbugs:jsr305:1.3.9)を実装するライブラリのオプション値に関する注釈を使用することです。
- @Nullable-注釈付きの値はオプションです。
- @Nonnullはそれぞれ反対です。
当然、両方の注釈は、オブジェクトとクラスのフィールド、メソッドの引数と戻り値、ローカル変数に適用できます。 したがって、これらの注釈は、値の必須の存在に関して型情報を補完します。
ただし、すべてに長い時間注釈を付けると、コードの可読性が大幅に低下します。 したがって、原則として、プロジェクトチームは
@Nullable
とマークされていないものはすべて必須であるという同意を受け入れます。 Guava、Guiceを使用した人は、このプラクティスをよく知っています。
以下に、このような抽象的なプロジェクトの可能なコードの例を示します。
import javax.annotation.Nullable; public abstract class CodeSample { public void correctCode() { @Nullable User foundUser = findUserByName("vasya"); if(foundUser == null) { System.out.println("User not found"); return; } String fullName = Asserts.notNull(foundUser.getFullName()); System.out.println(fullName.length()); } public abstract @Nullable User findUserByName(String userName); private static class User { private String name; private @Nullable String fullName; public User(String name, @Nullable String fullName) { this.name = name; this.fullName = fullName; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Nullable public String getFullName() { return fullName; } public void setFullName(@Nullable String fullName) { this.fullName = fullName; } } }
どこでも見ることができるように、参照参照でnullを取得できるかどうかは明らかです。
唯一の注意点は、現在のコンテキスト(たとえば、ビジネスプロセスの特定の段階)で、一般的な場合に何かが存在しなければならないことが確実にわかっているときに状況が発生することです。 私たちの場合、これはVasilyのフルネームであり、原則としてユーザーが利用できない場合がありますが、ビジネスロジックのルールに従ってここでは不可能であることを知っています。 このような状況では、単純なアサートユーティリティを使用します。
import javax.annotation.Nullable; public class Asserts { /** * For situations, when we definitely know that optional value cannot be null in current context. */ public static <T> T notNull(@Nullable T obj) { if(obj == null) { throw new IllegalStateException(); } return obj; } }
実際のJavaアサートも使用できますが、ランタイムに明示的に含める必要があり、構文があまり便利ではないため、慣れていませんでした。
継承と共分散/反分散に関するいくつかの言葉:
- 祖先メソッドの戻り値の型がNotNullの場合、オーバーライドされた継承メソッドもNotNullでなければなりません。 残りは有効です。
- 祖先メソッドの引数がNullableである場合、オーバーライドされた子孫メソッドもNullableでなければなりません。 残りは有効です。
実際、これですでに十分であり、(IDEまたはCIでの)静的分析は特に必要ありません。 しかし、IDEを機能させてください。彼らがそれを買ったのは、何の理由でもありません。 私はIntellij Ideaを使用することを好むので、それ以降のすべての例はその上にあります。
Intellij Ideaは人生をより良くします
デフォルトでは、Ideaはアノテーションを同様のセマンティクスで提供しますが、他のすべての人を理解しています。 これは、[設定]-> [検査]-> [起こりそうなバグ]-> {一定の条件と例外;
@NotNull/@Nullable
問題}。 両方の検査で、使用する注釈のペアを選択します。
以下は、前のコードの誤った実装での検査で見つかったエラーを強調表示した場合のIdeaの外観です。
IDEは非常に素晴らしいものになりました。IDEは2つのNPEを検出するだけでなく、それらを使用して何かを強制的に実行します。
すべて問題ないように見えましたが、Ideaビルトイン静的アナライザーは、受け入れたデフォルトのバインディングアグリーメントを理解していません。 彼女の観点から(および他の統計アナライザーと同様に)3つのオプションがここに表示されます。
- Nullable-値は必須です。
- NotNull-値はオプションです。
- 不明-バインドの意味については何もわかっていません。
そして、マークアップしなかったものはすべて不明と見なされます。 これは問題ですか? この質問に答えるには、NullableおよびNotNullのIdeaインスペクションが何を見つけることができるかを理解する必要があります。
- オブジェクトフィールドまたはメソッドにアクセスするときの潜在的にnull変数の逆参照。
- Nullable変数の引数をNotNullに渡します。
- NotNull変数の値がないかどうかの過剰なチェック。
- 値を割り当てるときのバインディングのパラメーターの不一致。
- ブランチの1つでNullable変数を使用してNotNullを返します。
これらの注釈でマークアップされていないライブラリメソッドから返される値は不明であるということは論理的です。 これに対処するには、割り当てるローカル変数またはフィールドに注釈を付けます。
引き続き慣行に従っている場合、コードではすべてがNullableオプションとしてマークされます。 したがって、最初のテストは引き続き機能し、多くのNPEから保護されます。 残念ながら、他のすべてのチェックは落ちました。 2番目のチェックは、引数として積極的にnullを受け入れ、このために設計されていない他の人のメソッドにnullを渡すメソッドを書くのが非常に好きな仲間に対して非常に役立ちますが、機能しません。
2番目のチェックの動作を復元するには、2つの方法があります。
- 「一定の条件と例外」検査の設定で、「nullを返す可能性のあるメソッドの@Nullableアノテーションを提案し、非アノテーション付きパラメーターに渡されたnull可能値を報告する」オプションを有効にします。 これにより、プロジェクト全体のすべての注釈なしメソッド引数がNotNullと見なされます。 始まったばかりのプロジェクトでは、このソリューションは完璧ですが、明らかな理由から、重要な既存のコードベースを使用してプロジェクトにプラクティスを実装する場合には適切ではありません。
-
@ParametersAreNonnullByDefault
アノテーションを使用して、メソッド、クラス、パッケージなどの特定のスコープで適切な動作を設定します。 このソリューションは、すでにレガシープロジェクトに最適です。 軟膏のフライは、パッケージの動作を設定するときに再帰がサポートされていないため、このアノテーションを一度にモジュール全体に掛けるべきではありません。
どちらの場合も、デフォルトでは注釈のないメソッド引数のみがNotNullになります。 フィールド、ローカル変数、戻り値は影響を受けません。
近い将来
Intellij Idea 14 EAPで既に機能する@TypeQualifierDefaultの今後のサポートは、状況を改善するために呼び出されます。 それらを使用して、
@NonNullByDefault
アノテーションを定義できます。これにより、すべてのデフォルトバインディングが決定され、同じスコープがサポートされます。 現在、再帰もありませんが、議論があります。
以下は、注釈付きの新しいスタイルのコードを使用してレガシーコードから作業する3つのケースを検査がどのように探すかを示しています。
明示的に注釈を付けます:
デフォルトでは、引数のみ:
デフォルトでは、すべて:
終わり
すべてがほぼ素晴らしくなりました。IntellijIdea 14のリリースを待つ必要があります。完全に幸福になるまでまだ足りないのは、Java 8より前にGenericで型に注釈を付ける機能と、Type注釈のサポートです。 場合によっては、ListenableFuturesおよびコレクションの非常に不足しているもの。
記事の量が非常に重要であることが判明したため、ほとんどの例は船外に残っていましたが、 ここで入手できます 。
更新しました。 外部ライブラリのオプション値に関するメタ情報を追加することは依然として可能です。