Java アプリケーションを開発していて誰もが NullPointerException を目にしたことがあるのではないでしょうか。
それほど NullPointerException はコードに混入しやすいバグの 1 つです。
では、その NullPointerException の対策として具体的に何か実施していることはありますか?
本記事では、NullPointerException が発生する原因と対策、さらに Jtest の静的解析で NullPointerException を検出する方法とルールのカスタマイズについて説明します。
NullPointerException とは
NullPointerException は、java.lang.RuntimeException クラスのサブクラスで Java プログラムの実行中に参照型変数を参照しようとした時に null 値であった場合、スローされるランタイム例外です。
Java プログラムで頻繁に発生する例外で、例外がスローされると、プログラムの処理は中断します。
アプリケーションにとって致命的な問題につながることも多く、CWE 4.5 や SEI CERT Oracle Coding Standard for Java では脆弱性の一つとしても扱われています。
Parasoft Jtest では、セキュリティ脆弱性の検出を目的としたルールセットもご用意しております。詳細は、以下の記事をご参照ください。
NullPointerException の原因と対策
では、NullPointerException は何故発生し、どうすれば発生しない安全なコードになるのでしょうか。
原因
NullPointerException が発生する主な原因として以下の場合があります。
- 参照型変数を間接参照したタイミングで未初期化、または明示的に null が代入されている場合
- 変数または式の間接参照は、以下のようなタイミングで生じます。
- null オブジェクトを利用したメソッドを呼び出し (a.method())
- null オブジェクトを利用したフィールドへアクセス (a.field)
- 未初期化、または null で初期化された配列の長さを参照 (arr.length)
- 未初期化、または null で初期化された配列の要素を取得 (arr[i])
- 変数または式の間接参照は、以下のようなタイミングで生じます。
- null 値を受け付けないメソッドに null 値を渡した場合
対策
NullPointerException を発生させないための対策として以下を意識します。
- 未初期化、または明示的に null が代入されている変数を参照しない
- 以下の点に注意してコードを実装します。
- 変数をnull以外で初期化する
- 配列は空で初期化する
- 戻り値にnullを返さない
- null 判定を行う
- java.util.Optional を使用する
- 以下の点に注意してコードを実装します。
- null を受け付けないメソッドに null を渡さない
ソースコードの品質向上を実現します。
Jtest の静的解析で NullPointerException を検出する
Parasoft Jtest には NullPointerException が発生するコードを検出する静的解析ルールがあります。
- NullPointerException を避ける [BD.EXCEPT.NP]
このルールを使用すると Jtest は未初期化の変数や null が代入された変数の間接参照、 null 値を受け付けないメソッドの引数に null 値を渡すアプリケーションの実行パスを検出します。
検出例1:戻り値が null
Jtest 解析結果とソースコード (一部抜粋):
DBServlet.java 43 行目から DataBase#getItemByTitle に遷移後、SQLException が throw された場合は null 値が戻り値として返却されます。
DBServlet#doGet では戻り値が null 値であることを想定しないまま DBServlet.java 45 行目で、戻り値を代入したオブジェクトを利用したメソッドを呼び出しである ret.iterator(); を行い NullPointerException がthrow されるため BD.EXCEPT.NP の違反を検出します。
修正例 1 (戻り値に null を返さない)
戻り値として null 値を返さないように DataBase#getItemByTitle を修正します。
修正例 2 (java.util.Optional を使用する)
java.util.Optional#ifPresent を使用して、戻り値が null の場合は処理が実行されないようにコードを修正します。
検出例2:null 値を受け付けないメソッドに null 値を渡す
Jtest 解析結果とソースコード (位置抜粋):
java.util.Dictionary#putはパラメーターに null 値を許可しないメソッドです。
String 変数 "strKey" を null 値のまま put メソッドに渡すと、NullPointerException がthrow されるため BD.EXCEPT.NP の違反を検出します。
修正例
java.util.Dictionary#put の引数を事前にチェックするようにコードを修正します。
ルールをカスタマイズする
NullPointerException を避ける [BD.EXCEPT.NP] には幾つかの設定可能なオプションがあります。
解析対象のコードや検出したい問題に合わせてオプションの設定を変更することでルールの挙動をカスタマイズすることができます。設定はテストコンフィギュレーションの BD.EXCEPT.NP の [ルールパラメーター] タブにあります。
null チェックによって変数が null であることがわかっている場合に違反をレポート
このパラメーター グループを使用すると、レポートされる違反の種類をカスタマイズすることができます。
- パラメーターが OFF の場合:
- 解析対象のソースコードにおいて実行パス上で変数に null 値が代入されているコードがある場合だけを違反としてレポートします。
- パラメーターが ON の場合:
- 解析対象のソースコードにおいて実行パス上で明示的に変数に null 値が代入されている場合だけでなく、変数の null チェックによって null ポインター参照が行われることが確実な場合も違反がレポートされます。
- 次の可視性を持つメソッド内部で null チェックが実行されている場合を除く: チェックボックス
- null チェックが設定した任意の可視性を持つメソッドで行われている場合は違反を検出しません。
以下のコードの場合パラメーターが ON の場合に違反を検出しますが、OFF の場合は違反を検出しません。
パラメーターとして null を受け入れないメソッド
登録した任意のメソッドに Jtest が null 値であるとみなしたパラメーターが渡された場合違反を検出します。
以下の checkStr メソッドを null を受け入れないメソッドとして登録します。
検出例:
通常、 checkStr メソッドに null 値が渡されても NullPointerException は発生しませんが、ルールのオプションに登録しているため、 null 値であるとみなした変数がパラメーターとして渡される箇所で BD.EXCEPT.NP の違反を検出します。
潜在的な null の返却
このパラメーターグループは、Java の標準パッケージ java.lang、java.io、および java.util のメソッドが null を返す可能性を、解析の際に考慮するかどうかを指定します。
以下のパラメーターを必要に応じて有効化/無効化することができます。
- NULL を返す可能性があり、防止するのが困難な Java SE SE メソッドを考慮する
- メソッドの戻り値として null 値を返す可能性があり、null 値の返却を防止できないメソッドを呼び出した場合、Jtest は該当のメソッドからの戻り値を null 値であると判断して解析を行います。
- 例:java.lang.Throwable#getCause 、 java.lang.System#getProperty
- NULL を返す可能性があるが、防止するのは困難ではない Java SE メソッドを考慮する
- メソッドの戻り値として null 値を返す可能性があるが、クライアントがメソッドのコードを編集してメソッドの戻り値として null 値を返却しないように変更できるメソッドを呼び出した場合、Jtest は該当のメソッドからの戻り値を null 値であると判断して解析を行います。
- 例:java.io.BufferedReader#readLine
- Java SE collection のアクセス メソッドが NULL を返却できるものとする
- Java SE Collection の null 値を返却可能性がある getter メソッドからの戻り値を null 値であると判断して解析を行います。
- 例:java.util.Map#get
- null を返す可能性のある外部関数
- 解析対象のコードから *.jar ファイル内の任意のメソッドを外部参照している場合、戻り値として null を返却する可能性のあるメソッドを登録すると、Jtest はメソッドからの戻り値を null 値であると判断して解析を行います。
- Jtest のルールは *.jar ファイル内のコードの実装を考慮しません。解析対象のコードが外部参照しているライブラリ群の中で null 値が返却される可能性があるメソッド情報を登録してルールを利用することで、より正確な解析を行うことができます。
- 解析対象のコードから *.jar ファイル内の任意のメソッドを外部参照している場合、戻り値として null を返却する可能性のあるメソッドを登録すると、Jtest はメソッドからの戻り値を null 値であると判断して解析を行います。
まとめ
NullPointerException の原因と対策、Jtest の静的解析を利用した検出についてご紹介しました。
ランタイムエラーはその性質上、実際にアプリケーションを動かすまで混入していることに気付けないことが殆どです。頻繁に目にするエラーだからこそ、今回ご紹介したような対策をしっかりとする必要があります。
対策の一つとして Jtest の NullPointerException を避ける [BD.EXCEPT.NP] を使用している方も、今回ご紹介したルールのカスタマイズで今よりも正確な解析が出来るかもしれません。
ぜひ、この機会にご利用のテストコンフィギュレーションの設定を見直してみてください。
Parasoft Jtestについて
Java対応静的解析・単体テストツール Parasoft Jtest
Jtestは、テスト工数の大幅削減とセキュアで高品質なJavaシステムの開発を強力にサポートするJava対応テストツールです。2,000個以上のコーディング規約をもとにソースコードを静的に解析し、プログラムの問題点や処理フローに潜む検出困難なエラーを検出します。さらに、JUnitを用いた単体テストについて、作成、実行、テストカバレッジ分析、テスト資産の管理といった単体テストに係る作業をサポートし、単体テストの効率化を促進します。