プログラムはjava.lang.NullPointerExceptionをキャッチしてはならない。実行時にNullPointerException例外がスローされたということは、nullポインタを参照するコードがプログラム中に存在することを意味しており、このようなコードは修正しなくてはならない(詳しくは「EXP01-J. null ポインタ参照しない」を参照)。nullポインタ参照が発生する根本原因を解決せずに、NullPointerExceptionをキャッチすることでnullポインタ参照を処理することが好ましくない理由はいくつかある。第1に、NullPointerExceptionをキャッチするほうが、値がnullかどうかを確認するコードを追加するより、性能上のオーバーヘッドがはるかに大きいということが挙げられる[Bloch 2008]。第2に、tryブロック中の複数の式がNullPointerExceptionをスローする可能性がある場合、どの式が例外を発生させたのかを判断することは困難もしくは不可能である。NullPointerExceptionをcatchするブロックは、tryブロック中のいかなる場所でスローされたNullPointerExceptionをも処理してしまうからである。第3に、NullPointerExceptionがスローされた後、プログラムが想定通りに動作する状態であることはまれである。NullPointerExceptionをキャッチし、ログに記録した後で(あるいは例外を抑制して)プログラムの実行を継続しようとしても、ほとんどの場合失敗する。
同様に、RuntimeException、Exception、Throwableもキャッチしてはならない。すべての実行時例外を処理することのできるメソッドはほとんど存在しない。メソッドでRuntimeExceptionをキャッチしていると、NullPointerExceptionやArrayIndexOutOfBoundsExceptionといった、メソッドの設計者が想定していない例外をも受け取ることになるかもしれない。多くの場合、catch節では例外を単にログに記録するもしくは無視し、通常のプログラム実行を継続しようとするが、これは多くの場合「ERR00-J. チェック例外を抑制あるいは無視しない」に違反することになる。実行時例外は多くの場合、制御フローの不備に起因する脆弱性がプログラムに存在することを意味する。このような脆弱性は修正すべきである。
違反コード (NullPointerException)
以下の違反コード例では、Stringを引数にとり、それが有効な名前である場合にtrueを返すisName()メソッドを定義している。有効な名前は、1つ以上のスペースで区切られた、大文字から始まる2つの単語であると定義されている。このメソッドは、引数として与えられた文字列がnullであるかどうかをチェックする代わりに、NullPointerExceptionをキャッチし、falseを返す。
boolean isName(String s) { try { String names[] = s.split(" "); if (names.length != 2) { return false; } return (isCapitalized(names[0]) && isCapitalized(names[1])); } catch (NullPointerException e) { return false; } }
適合コード
以下の適合コードでは、NullPointerException をキャッチするのではなく、引数のStringがnullでないかを明示的にチェックしている。
boolean isName(String s) { if (s == null) { return false; } String names[] = s.split(" "); if (names.length != 2) { return false; } return (isCapitalized(names[0]) && isCapitalized(names[1])); }
適合コード
以下の適合コードでは、引数のStringがnullでないかを明示的にはチェックしない。引数がnullである場合にはNullPointerExceptionをスローする。
boolean isName(String s) /* NullPointerException をスローする */ { String names[] = s.split(" "); if (names.length != 2) { return false; } return (isCapitalized(names[0]) && isCapitalized(names[1])); }
nullチェックを省略することにより、メソッドの呼出し側にfalseを返す場合よりもプログラムをより迅速に異常終了する。またnullチェックは呼出し側に任せることになる。nullチェックをせずにNullPointerExceptionをスローするメソッドは、引数がnullでないことという前提条件を呼出し側に課す必要がある。
違反コード (Null Object Pattern)
以下の違反コード例は、Henneyのロギングサービスに関するNullObjectパターンに基づくものである[Henney 2003]。ロギングサービスは2つのクラスから構成される。1つ目のクラスは、FileLogクラスを用いてファイルにメッセージを書き込むものであり、2つ目のクラスはConsoleLogクラスを用いてコンソールにメッセージを書き込むものである。Logインタフェースが定義するwriteメソッドは、各ログクラスにおいて実装される。メソッドの選択は、プログラムの実行時に行われる。これらのロギングメカニズムは、Serviceクラスで利用される。
public interface Log { void write(String messageToLog); } public class FileLog implements Log { private final FileWriter out; FileLog(String logFileName) throws IOException { out = new FileWriter(logFileName, true); } public void write(String messageToLog) { // ファイルにメッセージを書き込む } } public class ConsoleLog implements Log { public void write(String messageToLog) { System.out.println(messageToLog); // コンソールにメッセージを書き込む } } class Service { private Log log; Service() { this.log = null; // ロガーを指定しない } Service(Log log) { this.log = log; // 指定したロガーを設定 } public void handle() { try { log.write("Request received and handled"); } catch (NullPointerException npe) { // 例外を無視 } } public static void main(String[] args) throws IOException { Service s = new Service(new FileLog("logfile.log")); s.handle(); s = new Service(new ConsoleLog()); s.handle(); } }
各Serviceオブジェクトは、Logオブジェクトがnullである可能性を考慮しなくてはならない。なぜなら、クライアントプログラムはロギングを行わないかもしれないからである。この違反コード例では、NullPointerExceptionを無視するtry-catchブロックを使用することで、nullチェックを省略している。
このクラス設計は、NullPointerExceptionの発生を抑制しており、「ERR00-J. チェック例外を抑制あるいは無視しない」に違反している。また、nullであるLogオブジェクトを無視することはサーバの通常運用の範囲であるため、例外は例外条件に対してのみ使用すべきであるという設計方針にも違反している。
適合コード(Null Object パターン)
Null Objectデザインパターンは、明示的なnullチェックに取って代わる設計方針を提供する。null参照ではなく、安全な「nullオブジェクト」を使用することで、明示的なnullチェックを行う必要がなくなる。
以下の適合コードでは、Serviceクラスの引数を取らないコンストラクタが、Log.NULLが提供する「何もしない」動作を使用するように変更している。その他のクラスには変更はない。
public interface Log { public static final Log NULL = new Log() { public void write(String messageToLog) { // 何もしない } }; void write(String messageToLog); } class Service { private final Log log; Service() { this.log = Log.NULL; } // ... }
logをfinal宣言し、オブジェクトの初期化時に値が設定されるようにしている。
別の実装方法としては、アクセッサメソッドを使用し、現在のログへの参照とのすべてのやり取りを管理する方法も考えられる。ログを設定するアクセッサメソッドは、null参照の代わりにnullオブジェクトを使用し、取得したインスタンスが(null参照ではなく)、実際のロガーであるかnullオブジェクトであるかのどちらかであることを確認している。nullオブジェクトのインスタンスは不変であり、スレッドセーフである。
システムの設計によっては、「何もしない」動作をメソッドに実装するのではなく、メソッドから値を返さねばならない場合もある。一つのやり方は、メソッドが呼出し元へ戻る前に、例外をスローするオブジェクトを使用することである[Cunningham 1995]。これは、nullを返すアプローチに代わる、便利な手法である。
分散環境においては、nullオブジェクトはそのコピーを作成して渡さなくてはならない。遠隔からの呼出しにおいて、nullオブジェクトにアクセスする度に引数の値を評価するオーバーヘッドが発生するからである。また、分散環境で使用するnullオブジェクトはSerializableインタフェースも実装しなくてはならない。
このデザインパターンを誤用してセキュリティ上重要なメッセージが破棄されることが決してないように、このデザインパターンを使う場合にはその旨を明記しなくてはならない。
違反コード (除算)
以下の違反コード例のdivision()メソッドは元々、ArithmeticExceptionだけをスローするように宣言されていたと仮定する。一方、メソッドの呼出し側は、ArithmeticExceptionという特定の例外に限定せず、より一般的なExceptionをキャッチすることで、算術エラーを通知する。このようなコーディング作法にはリスクが伴う。なぜなら、メソッドの仕様が将来変更されると、呼出し側が処理しなければならない例外が増える可能性があるからである。以下のコードでは、改訂されたdivision()メソッドはArithmeticExceptionに加えてIOExceptionもスローする。しかしコンパイラは、IOExceptionに対応する例外ハンドラが存在しないということを診断できない。なぜなら、メソッドの呼出し側はそもそもExceptionをキャッチしており、IOExceptionもキャッチするからである。それゆえ、例外からのリカバリプロセスは、スローされる例外によっては不適切であるかもしれない。さらに、コードを書いたプログラマは、Exceptionをキャッチすると未チェック例外もキャッチしてしまうということを想定していない。
public class DivideException { public static void division(int totalSum, int totalNumber) throws ArithmeticException, IOException { int average = totalSum / totalNumber; // IOException をスローする可能性のあるその他の処理... System.out.println("Average: " + average); } public static void main(String[] args) { try { division(200, 5); division(200, 0); // ゼロ除算 } catch (Exception e) { System.out.println("Divide by zero exception : " + e.getMessage()); } } }
違反コード
以下の違反コード例では、ArithmeticExceptionをキャッチすることで前述の問題を解決しようとしている。しかし依然としてExceptionをキャッチしているため、予期せぬチェック例外や実行時例外をキャッチしてしまう。
try { division(200, 5); division(200, 0); // ゼロ除算 } catch (ArithmeticException ae) { throw new DivideByZeroException(); } catch (Exception e) { System.out.println("Exception occurred :" + e.getMessage()); }
DivideByZeroException は Exception を拡張した独自の型である。
適合コード
以下の適合コードでは、想定される特定の例外(ArithmeticExceptionおよびIOException)のみをキャッチしており、それ以外の例外はコールスタックの上位に伝播させている。
import java.io.IOException; public class DivideException { public static void main(String[] args) { try { division(200, 5); division(200, 0); // ゼロ除算 } catch (ArithmeticException ae) { // DivideByZeroException は Exception を拡張しており、チェックされる throw new DivideByZeroException(); } catch (IOException ex) { ExceptionReporter.report(ex); } } public static void division(int totalSum, int totalNumber) throws ArithmeticException, IOException { int average = totalSum / totalNumber; // IOException をスローする可能性のあるその他の処理... System.out.println("Average: "+ average); } }
ExceptionReporter クラスの詳細は「ERR00-J. チェック例外を抑制あるいは無視しない」を参照。
適合コード (Java SE 7)
Java SE 7では、単一のcatchブロックで複数の異なる例外をキャッチすることができるため、コードの冗長性を改善することができる。以下の適合コードでは、想定される特定の例外(ArithmeticExceptionおよびIOException)をキャッチし、これらを単一のキャッチ節で処理している。これら以外の例外は、try文中の次のcatch節に伝播される。
import java.io.IOException; public class DivideException { public static void main(String[] args) { try { division(200, 5); division(200, 0); // ゼロ除算 } catch (ArithmeticException | IOException ex) { ExceptionReporter.report(ex); } } public static void division(int totalSum, int totalNumber) throws ArithmeticException, IOException { int average = totalSum / totalNumber; // IOException をスローする可能性のあるその他の処理... System.out.println("Average: "+ average); } }
例外
ERR08-EX0: catchブロックでは、すべての例外をキャッチし処理した後で、それらの例外を再度スローする場合がある(たとえば、例外が信頼境界を越える前に、例外に含まれるセンシティブな情報を取り除く場合)。詳細は「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」およびCWE 7、CWE 388を参照のこと。そのような場合には、catchブロックでExceptionやRuntimeExceptoinをキャッチするのではなく、Throwableをキャッチすべきである。
以下のコード例では、すべての例外をキャッチし、独自のDoSomethingExceptionクラスにラップした上で例外を再度スローしている。
class DoSomethingException extends Exception { public DoSomethingException(Throwable cause) { super(cause); } // その他のメソッド }; private void doSomething() throws DoSomethingException { try { // 例外をスローする可能性のあるコード } catch (Throwable t) { throw new DoSomethingException(t); } }
例外のラップは、未知の例外を安全に処理するために用いられる一般的な手法である。この手法を実装したその他の例は、「ERR06-J. 宣言されていないチェック例外をスローしない」を参照。
ERR08-EX1: スレッドプール中のワーカースレッドやSwingのイベントディスパッチを行うスレッドのような、タスクを処理するスレッドでは、Runnableインタフェースのような抽象化を介して信頼できないコードを呼び出す場合にはRuntimeExceptionをキャッチしてもよい[Goetz 2006a]。
ERR08-EX2: 耐故障性(fault tolerance)やグレースフル・デグラデーション(graceful degradation)が求められるシステムにおいては、適切な抽象化レベルにおいて、Throwable等の一般的な例外をキャッチしてログに記録してもよい。以下にそのようなシステムの例を挙げる。
- 最も外側のレイヤーですべての例外をキャッチしてログに記録し、続けてコンピュータを再起動(warm-starting)して制御を継続するリアルタイム制御システム。プログラムの終了がセーフティクリティカルもしくはミッションクリティカルな結果をもたらすような場合には、このような手法が正当化される。
- 主要な各サブシステムの外へ伝播するすべての例外をキャッチし、後でデバッグを行うためにログに記録し、そのあとで他のサービスを走らせたまま問題のあったサブシステムをシャットダウンする(そしてより単純な、制限された機能のバージョンに置き換える)ようなシステム。
リスク評価
NullPointerExceptionをキャッチすると、null参照を隠蔽したり、プログラムのパフォーマンスが低下したり、理解が困難でかつ保守に手間のかかるコードを生み出す結果となる恐れがある。同様に、RuntimeException、Exception、Throwableをキャッチすると、その他の例外までもがキャッチされ、例外を適切に処理できくなる恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
ERR08-J | 中 | 高 | 中 | P12 | L1 |
翻訳元
これは以下のページを翻訳したものです。
ERR08-J. Do not catch NullPointerException or any of its ancestors (revision 116)