プログラミング応用b 第6回『例外』

【例外の伝播(でんぱ)】

●キャッチされなかった例外の行方

 ところで,Fig.3-(5)の例を見てみよう。tryブロックの中で,MyException型オブジェクトが投げられている(Fig.3-(5)①)。
しかし,MyException型例外をキャッチできるcatchブロックが存在しない。しかたないので,例外オブジェクトは投げられっ
ぱなしのまま,finallyブロックの内容を実行する(Fig.3-(5)②)。



 問題なのは,その後である。finallyブロック内の後処理部分(Fig.3-(5)②)は実行したものの,例外はキャッチされないままの状態である。
つまり,エラー処理は行われていないのである(finallyブロックは,エラーが起こったかどうかに関係なく行われる共通の後処理部分であって,
エラー処理を担っているのではないことに注意)。

 このように,例外がcatchブロックでキャッチされない状況はよく起こる。たとえば,次のコードの場合を考えてみよう。

try{  }
catch( MyException me ) {

	ここで,できるだけのエラー処理を行う
	throw me; // 再び例外を投げる

}


このコードのように,catchブロックの中で再び例外を投げるような場合も,その新たに投げられた例外をキャッチするcatchブロック
はこのコードの中には存在しない。finallyブロックで例外が投げられる場合についても同様のことが言える。

 こうした場合,強制的にtry-catch-finallyブロックの次の文,たとえばFig.3-(5)③から実行を強制的に再開すべきだろうか?
もちろん,エラー処理が行われていないのに,実行を再開しても,正常な処理が行われることは期待できないので,実行を無理矢
理再開することはしてはならない。では,どこから処理を再開できるのだろうか? そして,例外は投げられっぱなしになったま
まになるのだろうか? もちろん,投げられた例外オブジェクトがどこかでキャッチされないままだと処理は中断したままになって
しまう。

●例外の伝播(でんぱ)

 以上の例のように,例外がcatchブロックでキャッチされなかった場合,例外はさらにtry-catch-finallyブロックの「外」へ出て
行く
。これを,「例外の伝播(でんぱ)」と呼ぶ。もしその外側に別のtryブロックが有れば,伝播した例外をキャッチする機会が与えられる

 たとえば,Fig.3-(5)のプログラム部分の外側にさらにtry-catch-finallyブロックで囲んだものが,Fig.4-(1)である。処理の流れ
は,内側のtryブロックの中で例外が投げられ(Fig.4-(1)②),その例外をキャッチできるcatchブロックが無い場合は,finallyブ
ロックを実行(Fig.3-(1)③)した後,例外は外側のtryブロックへと出て行く(例外の伝播)。ここで,内側のtry-catch-finallyブロッ
クの次に続く処理は実行されずにスキップされる(Fig.3-(1)④)ことに注意。

 外側のtryブロックに伝播された例外は,外側のcatchブロックでキャッチできないか調べられることになる。この例では,外側
のcatchブロックでキャッチできるので,外側のcatchブロックで例外がキャッチされて,エラー処理が行われることになる(Fig.4-(1)⑤)。
その後,finallyブロックに処理が移り(Fig.4-(1)⑥),それが終わると,外側のtry-catch-finallyブロックを終了して,次の処理を
行っていく(Fig.4-(1)⑦)。



 同様に,try-catch-finallyブロックが入れ子になっている場合,内側のcatchブロックで例外が投げられても,外側のcatchブロッ
クで例外をキャッチすることが可能である(Fig.4-(2))。まず,内側のtryブロックで例外が投げられ(Fig.4-(2)①),対応するcatch
ブロックで例外がキャッチされ,さらに例外が投げられる(Fig.4-(2)②)。すると,finallyブロックの後処理が行われ(Fig.4-(2)③),
外側のtryブロックへ例外が伝播する。このとき,内側のtry-catch-finallyブロックに続く処理はスキップされる(Fig.4-(2)④)。
そして,外側のcatchブロックで例外がキャッチされ(Fig.4-(2)⑤),ついでfinallyブロックの内容を実行して(Fig.4-(2)⑥),
外側のtry-catch-finallyブロックに続く処理が実行されていく(Fig.4-(2)⑦)。

 このように,try-catch-finallyブロックが入れ子になっていて,外側のcatchブロックで例外をキャッチできればいいのだが,
やはり

void m( ) {

	try {
		throw new MyException( ); // ここで投げられる MyException型例外をキャッチできるcatchブロックは,このメソッド内には無い!
	}
	catch( YourException ye ) { } 

}

というように,メソッド内で例外をキャッチできるcatchブロックが存在しない場合はある。また,そもそもtryブロックで囲まれていない
範囲から例外が投げられる
void m( ) {

	throw new MyException( );   // 例外射出! でもここはtryブロックの内側じゃない!

}

というような場合も,このメソッド内で例外をキャッチできない。このようなケースでは,投げられっぱなしの例外はどこでキャッ
チされて,どこでエラー処理がされるのだろうか。そして処理の再開はどこから行われるのであろうか。

●メソッド外への例外の伝播

 実は,メソッドの中で例外をキャッチできない場合,メソッドはその例外を,そのメソッドを呼び出しているメソッドへと投げるので
ある。これが,まさにFig.1で示したメソッド間のエラー通知である。つまり,Javaではメソッドが自分で例外を処理しきれない場合,例
外を呼び出し元のメソッドに投げることによって,エラーを通知することができるのである。

 ただし,メソッドが例外を投げるためには,メソッドがどのような型の例外を投げるかを,メソッド宣言のthrows節に加えておかなけ
ればならない。その書式を,Fig.5に示す。書き方は簡単で,( )で囲まれた仮引数リストの後ろに,予約語throwsを書いて, そのメソッド
が投げる可能性のある例外型をカンマで区切って並べるだけである。



 もし,例外を投げる可能性のあるメソッドが,適切にthrows節を書いていないと,コンパイル時にエラーになる。たとえば,

class A {

	void m( ) throws E1 { }

	void h( ) { // エラー!
		m( );
	}

}

という場合,メソッドm( )はE1型例外を投げる可能性があると宣言されているので,m( )を呼び出しているメソッドh( )もE1型例外を投げ
る可能性がある。したがって,このままでは,h( )の宣言はエラーになってしまう。そのため,h( )の宣言でもthrows節をつけて,
class A {

	void m( ) throws E1 { }

	void h( ) throws E1 { // OK!
		m( );
	}

}

というように,宣言しなくてはならない。
 もし,例外をキャッチできるcatch節がある場合,メソッドがその例外を呼び出し元に投げることはないので,
class A {

	void m( ) throws E1 { }

	void h( ) {  // OK! 
		try {
			m( );
		}
		catch( E1 e ) {
			ここでエラー処理   // E1型例外はここでキャッチされて,もうメソッドh( )から出て行くことは無い!
		}
	}
 
}


という場合のh( )では,throws節をつけなくてもエラーにならない。


 なお,例外クラスE1,E2が例外クラスEのサブクラスであるとき,

void m( ) throws E1, E2 {

	// E1型, E2型を投げるかもしれない処理

}

というメソッド宣言のthrows節を,スーパークラスEを使って
void m( ) throws E {

	// E1型, E2型を投げるかもしれない処理

}


と書くこともでるが,これはあまり良いことではない
。このメソッドm( )がどのような例外を投げる可能性があるのかが,あやふやに
なってしまうからである。スーパークラスを使ってまとめて書いてしまうようなことはせずに,throws節には,できるだけ詳細に例外
型を明示するべき
である。

 なぜ,このようにメソッドが投げる例外の型を,throws節で明示しておく必要があるのだろうか。それは,必ず例外をキャッチする
ようにするためである。下図List 4は,①で例外が投げられているが,これはキャッチされない。実際に実行してみると,List 4-①
で例外が投げられた後,プログラムが強制終了してしまうことがわかる。つまり,投げられた例外がキャッチされない場合,プログラム
は強制的に終了してしまう
のである。

 そのため,プログラムを作成するときは,投げられた例外を確実にキャッチしなくてはならない。そのためには,どのメソッドがどの
ような例外を投げるかがはっきりと分かるようにしておかなければならない。これが,throws節でそのメソッドが投げる例外の型を明示
する理由である。

List 4 Uncaught.java




●チェックされない例外


 ところで,throws節に書かなくてもコンパイルエラーにならない特殊な例外型もある。それは,Error と RuntimeException およびそれらのサブ
クラス
である。そのためこれらは,(throws節で)チェックがされない例外と呼ばれる(Fig.2)。

 Error は,Java仮想マシンで重大な実行時エラーが起こったことを表している。たとえば,ソースファイルをコンパイルして生成される
クラスファイル(.classという拡張子のファイル)
が壊れているような場合などである。また,メモリ不足で新しいオブジェクトを生成する
ことにnew演算子が失敗したときなどに投げられる OutOfMemoryError もError のサブクラス VirtualMachineError のサブクラスである。

 こういったエラーは,予想外の場所で起こりえるし,一般のアプリケーションがエラー回復できるような種類のものではない。そのた
め,通常は,Errorおよびそのサブクラス型の例外をキャッチしようとしてはならない。こういった理由で,特にthrows節に明記する必要
は無いようになっているのである。

 また,RuntimeException は,Java仮想マシンの実行中に起こる軽度のエラーで,Javaのプログラムでは,どこでも起こりえるような
エラーを表す。たとえば,
A a = null;
a.m( );

というように,nullオブジェクトに対してメソッドを呼び出そうとした場合に投げられる NullPointerException型例外や,配列の範囲外
を参照しようとすると投げられる IndexOutOfBoundsException型例外などが RuntimeException のサブクラスである。これらの例外は,
多くはアルゴリズムを見直すことで解決できる。

 RuntimeException は,前述したとおり,頻繁にどこでも起こりえる例外で,言い換えれば,「プログラムを書くのが下手」であれば
どのメソッドも投げる可能性のある例外である。つまり,そんなものを全メソッドのthrows節にいちいち書くのは意味が無いし冗長に
なってしまう。そのため,throws節に書かなくてもよいようになっているのである。

まとめると次の様になる。
チェックされない例外 throws節に書かなくてもいい理由
Error およびそのサブクラス 予想の出来ないタイミングで起こる可能性がある。言い方を変えるとどのメソッドでもこの例外を投げてくる可能性があるので,わざわざthrows 節に明示する必要は無い。
RuntimeException およびそのサブクラス アルゴリズムの見直しで発生を予防できる例外である。言い方を変えると,書き方が万全では無いメソッドではこの例外を投げてくる可能性がある。メソッドを定義するプログラマの質を保証する手段は無いので,「どのメソッドでもこの例外を投げてくる可能性がある」と言え,わざわざthrows 節に明示する必要は無い。


 Error と RuntimeException およびそのサブクラスは,Javaのシステムが投げる例外である。そのため,プログラマはこれらのサブクラ
スを自分で作成して利用することはしてはいけない。通常は,Java APIで定義されている Exception のサブクラスを使うか,自分で
Exception のサブクラスを作って使用することになる。


●catchブロックの順序


次に進む