Java Language
Javaの落とし穴 - NullとNullPointerException
サーチ…
備考
値null
は、型が参照型であるフィールドの初期化されていない値のデフォルト値です。
NullPointerException
(またはNPE)は、 null
オブジェクト参照に対して不適切な操作を実行しようとするとスローされる例外です。そのような操作には、
-
null
ターゲットオブジェクト上でインスタンスメソッドを呼び出す、 -
null
ターゲット・オブジェクトのフィールドにアクセスし、 -
null
配列オブジェクトを索引付けしようとするか、その長さにアクセスしようとしましたが、 -
synchronized
ブロック内のnull
オブジェクト参照をミューテックスとして使用して、 -
null
オブジェクト参照をキャストし、 -
null
オブジェクト参照をアンボックスする -
null
オブジェクト参照をスローしnull
。
NPEの最も一般的な根本原因:
- 参照型を持つフィールドを初期化するのを忘れると、
- 参照型の配列の要素を初期化するのを忘れる
- 特定の状況で
null
を返すように指定された特定のAPIメソッドの結果をテストしません。
null
を返す一般的に使用されるメソッドの例には、
-
Map
APIのget(key)
メソッドは、マッピングを持たないキーで呼び出すとnull
を返しnull
。 -
ClassLoader
およびClass
APIのgetResource(path)
およびgetResourceAsStream(path)
メソッドは、リソースが見つからない場合はnull
を返しnull
。 - ガベージコレクタが参照をクリアした場合、
Reference
APIのget()
メソッドはnull
を返しnull
。 - 存在しないリクエストパラメータ、セッションまたはセッション属性などを取得しようとすると、Java EEサーブレットAPIのさまざまな
getXxxx
メソッドはnull
を返しnull
。
null
を明示的にテストするか、「Yoda Notation」を使用するなど、望ましくないNPEを回避するための戦略がありますが、これらの戦略では、実際に修正する必要のある問題を隠すという望ましくない結果が生じることがあります。
落とし穴 - プリミティブラッパーを不必要に使用すると、NullPointerExceptionが発生する可能性があります。
新しいJavaのプログラマーは、プリミティブ型とラッパーを同じ意味で使用することがあります。これは問題を引き起こす可能性があります。この例を考えてみましょう。
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
MyRecord
クラス1では、デフォルトの初期化を使用してフィールドの値を初期化しています。したがって、レコードをnew
すると、 a
およびb
フィールドはゼロに設定され、 c
およびd
フィールドはnull
に設定されnull
。
デフォルトの初期化されたフィールドを使用しようとすると、 int
フィールドは常に動作することがわかりますが、 Integer
フィールドは動作しない場合もあります。具体的には、失敗した場合( d
場合)は、右側の式がnull
参照をアンボックスしようとし、それが原因でNullPointerException
がスローされます。
これを見るにはいくつかの方法があります:
フィールド
c
とd
がプリミティブラッパーである必要がある場合は、デフォルトの初期化に頼るべきではないか、またはnull
テストする必要がありnull
。前者は、null
状態のフィールドに明確な意味がない限り 、正しいアプローチです。フィールドがプリミティブラッパーである必要がない場合は、プリミティブラッパーにするのは間違いです。この問題に加えて、プリミティブラッパーはプリミティブ型に比べて余分のオーバーヘッドを持ちます。
ここでの教訓は、本当に必要な場合を除き、基本ラッパー型を使用しないことです。
1 - このクラスは良いコーディングの例ではありません。例えば、うまく設計されたクラスはパブリックフィールドを持たないでしょう。しかし、これはこの例のポイントではありません。
Pitfall - 空の配列やコレクションを表すためにnullを使う
いくつかのプログラマーは、空の配列やコレクションを表すためにnull
を使用してスペースを節約することをお勧めします。少量のスペースを節約できるのは間違いありませんが、コードが複雑で壊れやすくなるということです。配列を合計する方法の2つのバージョンを比較する:
最初のバージョンは、メソッドを通常どおりにコーディングする方法です。
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
2番目のバージョンは、 null
を使用して空の配列を表す習慣がある場合に、メソッドをコーディングする必要がある方法です。
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
ご覧のとおり、コードはもう少し複雑です。これは、このようにnull
を使用するという決定に直接起因するものです。
次に、 null
可能性があるこの配列が多くの場所で使用されているかどうかを検討します。あなたがそれを使用する各場所で、 null
をテストする必要があるかどうかを検討する必要がありnull
。あなたがそこに存在する必要のあるnull
を逃した場合、あなたはNullPointerException
を危険にさらします。したがって、このようにnull
を使用すると、アプリケーションがより脆弱になります。プログラマのエラーの影響をより受けやすくなります。
ここでの教訓は、空の配列と空のリストを使用してそれが意味するときです。
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
スペースのオーバーヘッドは小さく、これは価値のあることであれば最小限に抑える他の方法があります。
落とし穴 - 予期せぬ空白を作る
StackOverflowでは、Answersに次のようなコードがよく表示されます。
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
しばしば、これは、 NullPointerException
を回避するために、このようなnull
をテストする "ベストプラクティス"であるアサーションを伴いnull
。
それはベストプラクティスですか?要するに:いいえ。
joinStrings
これを行うことをお勧めする前に、質問する必要があるいくつかの基本的な前提があります:
"a"または "b"がnullであるとはどういう意味ですか?
String
値はゼロ以上の文字である可能性があります。したがって、すでに空の文字列を表す方法があります。 null
は""
とは何か違う意味ですか?いいえの場合は、空の文字列を表す2つの方法があることが問題になります。
nullは初期化されていない変数から来たのですか?
null
は、初期化されていないフィールド、または初期化されていない配列要素から来る可能性がありnull
。値は設計によって、または偶然によって初期化されないことがあります。それが偶然だった場合、これはバグです。
ヌルは「知らない」か「欠損値」を表していますか?
ときにはnull
が真の意味を持つこともありnull
。例えば、変数の実際の値が不明であるか、利用できないか、または「オプション」であるかどうか。 Java 8では、 Optional
クラスはそれを表現するより良い方法を提供します。
これがバグ(または設計ミス)であれば、「良い」ものにする必要がありますか?
コードの解釈の1つは、空の文字列をその場所に使用することによって予期せぬnull
「良くする」ことです。正しい戦略ですか? NullPointerException
発生するようにして、例外をスタックのNullPointerException
キャッチしてバグとして記録する方が良いでしょうか?
「うまくいく」という問題は、問題を隠すか、診断するのが難しくなることです。
これはコード品質に効率的か良いか?
一貫して "make good"アプローチを使用すると、コードには多くの "守備的な"ヌルテストが含まれます。これは、読むのをより長くし、より困難にするでしょう。さらに、このテストと「うまくいく」のすべてが、アプリケーションのパフォーマンスに影響する可能性があります。
要約すれば
null
が意味のある値である場合、 null
場合のテストが正しい方法です。結果として、 null
値が意味を持つ場合、 null
値を受け入れるか返すメソッドのjavadocでこれを明確に文書化する必要がありnull
。
それ以外の場合は、予期しないnull
をプログラミングエラーとして扱い、開発者がコードに問題があることがわかるようにNullPointerException
発生させることをお勧めします。
Pitfall - 例外をスローする代わりにnullを返す
Javaプログラマの中には、例外を投げたり伝播したりするという一般的な嫌悪感を持っているものがあります。これにより、次のようなコードが生成されます。
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
では、それで何が問題なのですか?
問題は、 getReader
がReader
オープンできなかったことを示すためにnull
を特別な値として返すことです。戻り値は、使用する前にnull
かどうかを調べる必要がありnull
。テストが省略された場合、結果はNullPointerException
ます。
実際には3つの問題があります。
- あまりにも早く
IOException
がキャッチされました。 - このコードの構造は、リソースを漏らすリスクがあることを意味します。
- 「実際の」
Reader
が返されなかったため、null
が返されました。
実際、例外をこのように早期に捕らえる必要があると仮定すると、 null
を返す代替手段がいくつかありました。
-
NullReader
クラスを実装することは可能です。あたかもリーダーがすでに「ファイルの終わり」の位置にあるかのようにAPIの動作が振る舞うようなものです。 - Java 8では、
getReader
をOptional<Reader>
を返すように宣言することができます。
落とし穴 - I / Oストリームを閉じたときにI / Oストリームが初期化されていないかどうかをチェックしない
メモリリークを防ぐために、入力ストリームまたは出力ストリームを閉じることを忘れないでください。これは通常、 catch
部分のないtry
- catch
- finally
文で行います。
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
上記のコードは無害に見えるかもしれませんが、デバッグを不可能にする欠陥があります。 out
が初期化out
た行( out = new FileOutputStream(filename)
)が例外をスローすると、 out.close()
が実行さout
とout
がnull
になり、 NullPointerException
ます。
これを防ぐには、ストリームを閉じる前にストリームがnull
でないことを確認するだけです。
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
より良いアプローチは、リソースを-with-withでtry
ことです。これは、 finally
ブロックを必要とせずにNPEを投げるために0の確率でストリームを自動的に閉じます。
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
落とし穴 - NodaPointerExceptionを避けるために "Yoda記法"を使う
StackOverflowに投稿されたサンプルコードの多くには次のようなスニペットが含まれています:
if ("A".equals(someString)) {
// do something
}
someString
がnull
場合、 NullPointerException
するのを「防止」または「回避」しnull
。さらに、
"A".equals(someString)
よりも良い:
someString != null && someString.equals("A")
(より簡潔で、場合によってはより効率的かもしれませんが、以下で議論するように、簡潔さは否定できます。)
しかし、実際の落とし穴は、習慣の問題としてNullPointerExceptions
を避けるために Yodaテストを使用しています。
"A".equals(someString)
と書くとsomeString
がnull
になるケースが実際にはsomeString
ようになりnull
。しかし、別の例( Pitfall - "予期しない "良い "良い"を作る)が説明するように、 "良い" null
値を作ることはいろいろな理由で有害な可能性があります。
これは、ヨーダの条件が「ベストプラクティス」ではないことを意味します1 。 null
が予想されない限り、ユニットテストの失敗(またはバグレポート)を得るためにNullPointerException
発生するようにしてください。これにより、予期せぬ/望ましくないnull
原因となったバグを見つけて修正することができます。
Yoda条件は、テストしているオブジェクトがnull
を返すとして文書化されているAPIから来ているため、 null
が予想される場合にのみ使用する必要がありnull
。そして間違いなく、それが強調表示することができますので、テストを表現するあまり、かなりのいずれかの方法を使用するより良いかもしれないnull
あなたのコードを検討している誰かにテストを。
1 - Wikipediaによると、 「最良のコーディング慣行は、ソフトウェア開発コミュニティが時間をかけて学んだ、ソフトウェアの品質を向上させるのに役立つ非公式のルールのセットです」 。 Yodaの表記を使ってもこれは成立しません。多くの場合、コードが悪化します。