![]() ![]() ![]() ![]() |
例外を使用したエラー処理 |
例外(exception)という用語は、「例外的なイベント」の短縮形である。 次のように定義することができる。
定義: 例外は、命令の正常な流れを粉砕するプログラムの実行中に発生するイベントである。
多くの種類のエラーが例外の原因となる。問題は、ハードディスクの破壊のような重大なハードウェアエラーから、配列要素の境界を越えてアクセスしようとする単純なプログラミングエラーまで及んでいる。 このようなエラーが Java メソッド内で発生すると、メソッドは例外オブジェクトを生成し、ランタイムシステムにそれを渡す。 例外オブジェクトは、オブジェクトの型エラーが発生した時のプログラムの状態を含む例外についての情報を持っている。 そしてランタイムシステムは、エラーを処理するコードを探す。 Java の用語では、例外オブジェクトを作成して、ランタイムシステムにそれを渡すことを、例外をあげる(throw)と言う。
メソッドが例外をあげた後、ランタイムシステムは例外の処理を行なう誰かを見つける動作に飛ぶ。 例外を処理する可能性のある「誰か」のセットとは、エラーが発生したメソッドの呼び出しスタックの中にあるメソッドのセットである。 ランタイムシステムは、エラーが発生したメソッドから始めて、適切な例外ハンドラ(exception handler)を含むメソッドが見つかるまで、呼び出しスタックを通して後方に検索する。 例外ハンドラは、あげられた例外の型がハンドラによって処理される例外の型と同じであるなら、適切であると考えられる。 したがって例外は、適切なハンドラが見つかって呼び出しメソッドの一つが例外を処理するまで、呼び出しスタックを通してさらに外側のメソッドに伝播して行く。 例外ハンドラを選択することは,例外をキャッチする(catch)と言われる。
呼び出しスタック上のすべてのメソッドを徹底的に検索し,ランタイムシステムが適切な例外ハンドラを見つけられなかった場合は、ランタイムシステム(従って Java プログラム)は終了する。
例外を使用してエラーを管理することによって、 Java プログラムは伝統的なエラー管理技法に比べて次のような利点がある。
利点 1 :「通常の」コードとエラー処理コードを分離する
伝統的なプログラミングでは、エラーの検出、報告、処理は紛らわしいスパゲティコードになりやすい。 例えば、メモリにファイル全体を読み込む関数を考えると、関数は次のようになる。
readFile { open the file; determine its size; allocate that much memory; read the file into memory; close the file; }一見するとこの関数は十分単純に見えるが、次のようなエラーの可能性をすべて無視している。
- もしファイルがオープンできないと、何が起きるか?
- もしファイルの長さが決められないと、何が起きるか?
- もし十分なメモリが割り当てられないと、何が起きるか?
- もし読み込みに失敗したら、何が起きるか?
- もしファイルがクローズできないと、何が起きるか?
read_file 関数内でこれらの疑問に答えるためには、エラーの検出、報告、処理を行う多くのコードを追加しなければならない。 関数は次のようになる。
errorCodeType readFile { initialize errorCode = 0; open the file; if (theFileIsOpen) { determine the length of the file; if (gotTheFileLength) { allocate that much memory; if (gotEnoughMemory) { read the file into memory; if (readFailed) { errorCode = -1; } } else { errorCode = -2; } } else { errorCode = -3; } close the file; if (theFileDidntClose && errorCod == 0) { errorCode = -4; } else { errorCode = errorCode and -4; } } else { errorCode = -5; } return errorCode; }エラー検出処理を組み込んだので、オリジナルの7行(太字で表示)は29行のコードに膨張した。 ほとんど400パーセントの膨張である。 もっと悪いことに、エラー検出,報告,復帰と処理が多すぎてオリジナルの7行のコードはこうした処理のコードの乱雑さの中に埋もれてしまう。 さらに悪いことに、コードの論理的な流れも埋もれてしまうので、コードが正常な動作(関数が十分なメモリの割り当てができなかった場合にファイルは本当にクローズするか?)を行っているかどうかを見極めるのは困難である。さらに、関数を作成した三ヶ月後に関数を修正した後、コードが正常な動作を続行するように保証するのは困難である。 多くのプログラマはプログラムに障害が発生すると、エラーが「報告」されるという当然行なうべき処理を単に無視することによってこの問題を「解決」する。
Java はエラー管理の問題に簡潔な解を提供する。それが例外である。 例外により、コードの主な流れを書き、他のところで十分に例外的なケースを扱うことができる。read_file 関数が伝統的なエラー管理技法の代わりに例外を使用すると、次のようになる。
readFile { try { open the file; determine its size; allocate that much memory; read the file into memory; close the file; } catch (fileOpenFailed) { doSomething; } catch (sizeDeterminationFailed) { doSomething; } catch (memoryAllocationFailed) { doSomething; } catch (readFailed) { doSomething; } catch (fileCloseFailed) { doSomething; } }例外によりエラーの検出、報告、処理の作業が軽減されることはないので注意すること。 例外は、異常が起きたときに行うすべての詳細を区別する手段を提供する。
さらに、このプログラムでのエラー管理コードの膨張ファクタは、前述の例の400パーセントと比較して、約250パーセントである。
利点 2 :呼び出しスタックにエラーを伝達する
例外の 2 番目の利点は、メソッドの呼び出しスタックにエラー報告を伝達する能力である。readFile メソッドは、メインプログラムで作成された一連の入れ子のメソッド呼び出しの4番目のメソッドであると仮定する。つまり、method1 が method2 を呼び出し、method2が method3 を呼び出し、method3が最後にreadFile を呼び出す。
method1 { call method2; } method2 { call method3; } method3 { call readFile; }またmethod1 は 、 readFile 内で発生するエラーに関与する唯一のメソッドであると仮定する。 伝統的なエラー通知技法では、エラーコードが,エラーに関与する唯一のメソッドであるmethod1 に最終的に到達するまで、呼び出しスタックに readFileが返すエラーコードを伝達するように method2 と method3に強制する。
method1 { errorCodeType error; error = call method2; if (error) doErrorProcessing; else proceed; } errorCodeType method2 { errorCodeType error; error = call method3; if (error) return error; else proceed; } errorCodeType method3 { errorCodeType error; error = call readFile; if (error) return error; else proceed; }すでに学習したように、Java ランタイムシステムは、特定の例外の処理に関係しているすべてのメソッドを見つけるために、呼び出しスタックを通して後方に検索する。 Java メソッドはその中であげられたどんな例外も「避ける」ことができる。こうした場合、この例外を受けるメソッドがないか呼び出しスタック上をさらに探して行く。 したがって、エラーに関心を持つメソッドだけが、エラーの検出について心を配れば良い。
method1 { try { call method2; } catch (exception) { doErrorProcessing; } } method2 throws exception { call method3; } method3 throws exception { call readFile; }しかし、擬似コードからわかるように、例外を避けるには「仲介人」メソッドの一部分で何らかの手続きを必要とする。 メソッド内であげられる確認済例外はすべて、そのメソッドの public なプログラムインタフェース一部としてthrows 節で指定されなければならない。 こうして、メソッドはあげる例外についてその呼び出し側に知らせる。それにより、呼び出し側はこの例外について行うことを理性的にかつ意識的に決定することができる。
これらの2つのエラー管理技法の膨張ファクタとコードの分かりにくさのファクタにおける差についてもう一度注目すること。 例外を使用するコードはよりコンパクトで、理解しやすい。
利点 3 :エラーの型とエラーの識別をグループ化する
しばしば例外はカテゴリあるいはグループに分かれる。 例えば、それぞれが配列を操作する時、発生する可能性のある特定のエラータイプを表す例外のグループをイメージすることができる。インデックスが配列のサイズの範囲外を指している、配列に挿入されている要素の型が間違っている、あるいは探している要素は配列に入っていない。 さらに、あるメソッドはカテゴリ(すべての配列例外)内に入るすべての例外を処理したく、他のメソッドは特定の例外(無効なインデックス例外)を処理したいとイメージすることができる。
Java プログラム内であげられるすべての例外は最上位オブジェクトなので、例外のグループ化あるいはカテゴリ分類は当然の結果としてクラスとスーパークラスになる。 Java 例外は Throwable でなければならない。つまり、それらは Throwable のインスタンスであるか、または Throwable の子孫でなければならない。 他の Java クラスについては、 Throwable クラスのサブクラスとサブクラスのサブクラスを作成することができる。 それぞれの「リーフ」クラス(サブクラスがないクラス)は、特定の型の例外を表し、それぞれの「ノード」クラス(1つ以上のサブクラス持っているクラス)は関連した例外のグループを表す。
例えば次の図では、 ArrayException は Exception( Throwable のサブクラス) のサブクラスで、3つのサブクラスを持っている。
InvalidIndexException 、 ElementTypeException および NoSuchElementException はすべてリーフクラスである。 それぞれが、配列を操作する時に、発生する可能性がある特定のエラーの型を表す。 メソッドはその特定の型(その直接のクラスあるいはインタフェース)に基づいた例外をキャッチすることができる。 例えば、無効なインデックス例外だけを処理する例外ハンドラは、次のような catch 文を持つ。
catch (InvalidIndexException e) { . . . }ArrayException はノードクラスであり、配列オブジェクトを操作する時に発生する可能性のあるすべてのエラーを表す。これには、そのサブクラスによって特定されるエラーが含まれる。 メソッドは、 catch 文でその例外のスーパークラスのいずれかを指定することによって、そのグループあるいは汎用的な型に基づいた例外をキャッチすることができる。 例えば、特定の型にかかわらず、すべての配列例外をキャッチするためには、例外ハンドラは ArrayException 引数を指定する。
catch (ArrayException e) { . . . }このハンドラは InvalidIndexException 、 ElementTypeException および NoSuchElementException を含めてすべての配列例外をキャッチする。 例外ハンドラパラメータe を問い合わせることによって、どの型の例外が発生したかを正確に見つけだすことができる。 このハンドラを使用して、どんな Exception でも処理する例外ハンドラを設定することさえも可能である。
catch (Exception e) { . . . }ここで示したようにあまりにも汎用的な例外ハンドラを使用すると、予期していなかったゆえにハンドラ内で正しく処理されない例外をキャッチして処理することで、コードがエラーを発生しやすくなる。 原則として汎用的な例外ハンドラを書くことを勧めない。
見てきたように、汎用的な方式で例外のグループを作成し例外を処理したり、あるいは例外を区別するために特定の例外型を使用し、厳密な方式で例外を処理することができる。
次は何か?
例外とは何であるか、および Java プログラムで例外を使用する利点について理解できたので、次はその使用方法について学習する。
![]() ![]() ![]() ![]() |
例外を使用したエラー処理 |