前回の記事はこちら↓
Effective Java読書会8日目- プログラミング一般
[扱うテーマ一覧]
項目57 例外的状態にだけ例外を使用する
項目58 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する
項目59 チェックされる例外を不必要に使用するのを避ける
項目60 標準例外を使用する
項目61 抽象概念に適した例外をスローする
項目62 各メソッドがスローするすべての例外を文書化する
項目63 詳細メッセージにエラー記録情報を含める
項目64 エラーアトミック性に努める
項目65 例外を無視しない
##項目57 例外的状態にだけ例外を使用する
- ArrayIndexOutOfBoundsExceptionを使用して配列のループ処理の脱出するなどアホな事はしない(あたりまえ)
- APIの設計時に例外の検査メソッドなどを検討しておくと、APIの使用者に優しい
##項目58 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する
「回復可能な状態」という表現がややこしい。。。
そもそも「チェックされる例外」とは何か?
チェックされる例外(検査例外)
RuntimeException以外のExceptionクラス(FileNotFoundException等)
必ず任意の例外処理を記述しないといけない。記述がなければコンパイルエラーが発生。
API側でthrowすることで呼び出し側にエラーが起こりうる可能性を示し、エラーが発生したときの振る舞いを呼び出し側に委ねる。
FileNotFoundもAPI側としてはファイルが無いだけで、処理自体は続行可能なので「回復可能な状態」として判断する。(対象ファイルの重要度を呼び出し側に委ねる形)
実行時例外(非検査例外)
RuntimeExceptionを継承したクラス(ArrayIndexOutOfBoundsException等)
例外処理の記述は不要。
APIを使用する側がAPIで約束されていない使い方をしたときに発生する。
呼び出す側の実装次第で回避可能なことを示す。
Errorクラス(非検査例外)
OutOfMemoryなど、例外処理では復旧できない種類の例外クラス
JVMでは回復できないエラーが約束されているので、新たなErrorサブクラスの作成は避ける。
##項目59 チェックされる例外を不必要に使用するのを避ける
検査例外の濫用はAPI呼び出し側の負担になる。
呼び出し側への負担を正当化できるのは下記2つの条件が満たされた場合と考える。
- APIを適切に使用しているのにも関わらず、例外発生を防ぐことはできない場合
- 例外に直面したときに有用な対処ができる場合
基本的に例外が発生したときにAPI呼び出し側で何ができるかを検討して、何もできなければ非検査例外を使用すべき。
例としてはCloneNotSupportedExceptionが悪例の一つ。検査例外となっているが発生しても何もできない。
try {
obj.action(args);
} catch (TheCheckedException e) {
//例外状態
}
if (obj.actionPermitted(args)) {
obj.action(args);
} else {
//例外状態
}
obj.action(args); //例外が発生したら終了
##項目60 標準例外を使用する
標準例外 = JDKで提供されている例外クラス。
既存の例外を使用する利点
- APIを学んで使用するのが容易
- APIを使用するプログラムが読みやすい
- 例外クラスが少なければリソース的にも優しい
最も再利用されている例外の例
java.lang.IllegalArgumentException
「回数」なのにマイナスの値を渡したときなどの不正な引数を渡されたことを示す例外。
java.lang.IllegalStateException
初期化される前に呼び出された、などの状態不正を示す例外。
java.lang.NullPointerException
ぬるぽ。
java.lang.IndexOutOfBoundsException
配列の範囲外を扱ったときの例外。
java.util.ConcurrentMdificationException
予期せず並行処理されたときの例外。
java.lang.UnsupportedOperationException
インターフェースでサポート外の処理をしたときの例外。
(追加のみのListに対して要素を削除しようとした時などに使用)
これらを再利用するときはセマンティクスに基づいて要求に合致するときのみ。
エラーの情報を付加したいのであれば、自由に既存の例外をサブクラス化して利用するのもOK。
##項目61 抽象概念に適した例外をスローする
例外翻訳 = 上位レイヤーで発生した例外を下位レイヤーでcatchし、情報を付加してthrowすること。
例えば、java.util.AbstractSequentialListのget()メソッドでは範囲外の要素を取得しようとした場合にNoSuchElementExceptionからIndexOutOfBoundsExceptionに例外翻訳している。
この手法は濫用はしない。下位レイヤーAPIで例外が発生しないように作るべき。
それが不可能であれば上位レイヤーのAPIで例外を処理して同時にログ出力することでエンドユーザーと問題を隔離できる。
例外連鎖 = 例外翻訳するときに上位例外へ下位の例外をオブジェクトとして格納してthrowすること。
上位例外は格納された下位例外から原因を取得するメソッドを提供する。
標準例外はコンストラクタを使用して例外連鎖し、原因となった例外はThrowable#getCause()で取得できる。
例外翻訳と例外連鎖を使いこなせるようにすれば開発・運用が容易になる。
##項目62 各メソッドがスローするすべての例外を文書化する
検査例外は発生条件ごとに個別に宣言して、javadocの@throwタグを使用して例外の発生条件などをそれぞれ文書化する。
横着して throw Exception や throw Throwable するとAPI使用者へ大変不親切なので絶対に使用しない。
非検査例外も検査例外と同じく文書化する。
(このあとイマイチ良く分からない・・・)
##項目63 詳細メッセージにエラー記録情報を含める
一言で言うと、例外のtoString()は超重要。
プログラム中でcatchされなかった例外によってプログラムが終了した場合、最終的にその例外のスタックトレースを表示し、そのスタックトレースにはtoString()のメッセージが含まれているため。
例外の原因を特定するためには全てのパラメータ・フィールド値を出力するべきだが、不要な情報は含めない。(行番号など)
あくまで原因はソースコードを読んで把握するべきなので、詳細メッセージは分かりやすさよりも情報の密度が大事。
実装においては必要な情報をコンストラクタで要求するイディオムを推奨。(IndexOutOfBoundsExceptionの例を参照)
##項目64 エラーアトミック性に努める
エラーアトミック = 処理が失敗したときにオブジェクトをエラー発生前の状態に戻すこと
呼び出し元が例外から回復することを期待している検査例外に対しては特に求められる。
実現方法として以下の方法がある。
- 不変オブジェクトを設計する
- オブジェクトが不変なのでエラーアトミック性としてコストが不要。
- 処理前に確認する
- オブジェクトを変更する前に例外をスローする。
- 回復コードを書く
- エラーは発生した時に回復用ロジックを書く。(主に永続的なデータ構造に使用)
- 一時コピーを使用する
- オブジェクトの一時コピーを作成し、そのオブジェクトに対して処理して無事完了したら上書きする。
- 例としてはjava.util.Collection#sort()メソッド(ソート前に配列に出力し、元のリストには影響させない)
エラーアトミック性はあれば望ましいが必ず達成可能とは限らない。
不整合な状態から回復ができないこともあったり、エラーアトミック性保持のためコストがかかることもある。
エラーアトミック性が保持できない場合はそのことをAPI仕様書に書くべきである。(現実的に可能?)
##項目65 例外を無視しない
try {
obj.action(args);
} catch (SomeException e) {
}
上記のように空のcatchブロックで例外を無視するなど言語道断。
例外を無視するなら最低でも無視するのかをコメントで残すべき。
無視するのが適切かもしれないケースとしては、FileInputStreamのクローズ処理。
ファイルの状態変更がなかったり、すでに必要な情報を読み出している場合などがあるが、それでも例外を記録することで頻度などの調査に役立つ。