オブジェクト指向プログラミングb 第4回 |
●例外「もっとJavaを学ぶ 第2回 (2003年5月 )」
【エラー処理の課題】
プログラムにとって必要不可欠でありながら,扱いが難しいもののひとつが,エラー処理である。
どのようにエラーを回復するか,という点については,それぞれのエラーごとに対応が違うので,
ここではふれない。ここでは,エラー処理を行うためのメカニズムについての課題を考えてみよう。
●エラー処理記述の複雑さ
エラー処理を行う場合,もっとも原始的な方法は,エラーが起こってないかどうかをif文でチェッ
クし,エラーが起こっていない場合は通常の処理を行い,エラーが起こっていた場合は,else節でエ
ラー処理を行うという方法である。
たとえば,List 1のように,4つの処理を実行している場合を考えてみよう。この例では,処理1か
ら処理4まで,順序よく処理が記述されていて(List 1-(1)),処理の流れが明確である。
しかし,この例では,エラー処理は一切行っていない。そこで,List 1の各処理ごとに
・エラーが起こっていないかどうかの検査
・エラーが起こっていた場合のエラー処理
を追加した例が,下のList 2である。
処理1〜処理4が行われている部分(List 2-(1))は,List 1-(1)と比べて,非常に見にくいものにな
っている。これは,エラー検査を行うif-else文で各処理が分断されているからである。そして,エラ
ー処理を行っているList 2-(2)の部分も,どこでどのエラーを処理しているか非常に分かりづらくな
っている。
このように,if文でエラーの検査を行う古典的な手法は,本来の処理の記述を複雑にしてしまうと
いう問題をかかえていると言える。
ところが,今回学習する「例外」の仕組みを使うと,下図右のように
通常処理とエラー処理部分が綺麗に分離されて分かりやすくプログラム
が書けるようになるのである。
●どこでエラー処理を行うか
エラーが起こるのは,プログラムを実行しているときである。つまり,メソッドを実行しているとき
である。今ここで,あるメソッドを実行中に,エラーが起こったとしよう。このとき,そのメソッドで
完全にエラー対処を行うことができるかというと,かならずしもそうではない。場合によっては,呼び
出し元のメソッドにエラー処理を頼まなくてはならない場合もある。
例として,メソッドm1( )がメソッドm2( )を呼び,さらにメソッドm2( )がメソッドm3( )を呼んでい
る状況を考えてみよう。たとえば,
class A { void m1( ) { m2( ); } void m2( ) { m3( ); } void m3( ) {} }というような場合である。下図Fig.1は,この状況でm3( )でエラーが起こった様子を示している。m3( )は,
【例外処理の基礎】
例外(exception)とは,文字通り,「例外的な出来事」,すなわちエラーのことである。Javaの持つ例外
処理メカニズムでは,処理の実行中にエラーが発生した場合,エラーの内容を表す例外オブジェクトと呼ば
れるオブジェクトを,エラー通知に使う。
この例外オブジェクトを使ってエラーの通知をすることを,“例外を投げる(スロー,throw)”と言う。
例外オブジェクトは,エラーを伝える通信筒と思えばよい。
●例外オブジェクトの型
例外オブジェクトは,java.langパッケージに用意された,特別なクラスThrowableのサブクラスのオブジェ
クトである。下図Fig.2に,例外オブジェクトのクラスの継承関係を示す。Throwableは,すべての例外クラ
スのスーパークラスである。
Throwableには,ErrorとExceptionの2つのサブクラスが定義されている。また,Exceptionには,RuntimeException
というサブクラスが用意されている。ErrorとRuntimeExceptionは,Java仮想マシンが通知してくる特別な
例外を表している。
つまり,一般のプログラマは,Java APIで用意されているExceptionのサブクラスを使うか,Exceptionの
サブクラスを自分で定義するかして使うようにする。
●例外を投げるthrow文
実際に例外を通知する(=例外を投げる)には,throw文(下図Fig.3-(1))を使用する。たとえば,Exception
のサブクラスMyExceptionを定義してあるとして,このMyException型の例外オブジェクトを“投げる”には,
throw new MyException( );
などとする。
例外が投げられると,処理はそこで中断される。そして,エラーを処理できるところが,その例外オブジェクト
を受け取って(キャッチ,catch),処理を再開し,エラー処理を行う。イメージ的には,エラーが起こった部分で
エラーの内容を記したボール(例外オブジェクト)を,「誰かエラー処理してね」と言って,投げるような様子を想
像するとよいだろう。
●tryブロック
例外処理の構文を,Fig.3-(2)に示す。形式的には,予約語のついたブロックがつらなった形になっている。
最初のブロックがtryブロックである(Fig.3-(2)<①>)。このtryブロックの中に通常の処理を書いておく。つまり,
この部分は通常の処理を“試す(try)”部分なのである。この通常の処理の中でエラーが起きた場合,例外を
“投げる”ことになる。
●catchブロック
tryブロックに続くのが,catchブロックである(Fig.3-(2)<2>)。tryブロックで投げられた例外オブジェクト
は,このcatchブロックで“受け取る”ことができる。catchブロックは,返り値なしのメソッドのようなもので,
catch( MyException e ) {}
というように,例外オブジェクトを受け取るための仮引数を1個だけ持つことができる(ここで,MyExceptionは,
Exceptionのサブクラスとする)。そして,受け取った例外オブジェクトを参照するなどして,エラー処理を行う。
catchブロックは,0個以上,繰り返して複数書くことが可能である。たとえば,
try { throw new MyException( ); /* MyException型オブジェクトを投げる */ }
catch( MyException me ) {}
catch( Exception e ) {}
という具合である。
このように,複数のcatchブロックがある場合,最初のcatchブロックから順序よく見ていって,投げられた
例外オブジェクトの型と一番最初に一致したcatchブロックが選択される。上の例では,1番目と2番目のcatch
ブロックの仮引数型は,それぞれ,MyException,Exceptionとなっている。
投げられた例外オブジェクトは,MyException型なので,型だけ見れば,MyExceptionもExceptionも一致す
る(MyExceptionが,Exceptionのサブクラスであるため)。しかし,1番目のcatchブロックの方が先に一致する
ので,実際には,1番目のcatchブロックが選択されることになる。そのため,1番目のcatchブロックの仮引数
meがtryブロックで投げられた例外オブジェクトを参照するようになり,1番目のcatchブロックの冒頭から処理
が再開される。
ちょっと注意しなければならないのは,
try { } catch( Exception e ) { } catch( MyException me ) { } // このcatchブロックは絶対に実行されない
try{ (1)前処理 (2)中心的な作業 } catch( 例外型 仮引数 ) { エラー処理 } finally { (3)後処理 }
●例外発生時の処理の流れ
基本的な例外処理の流れを,下図Fig.3-(3),(4)の例でおさらいしてみよう。ここで,MyExceptionとYourException
は,Exceptionクラスの直接のサブクラスであるとする(Fig.3-(3)<①>)。
最初にFig.3-(3)の例を見てみよう。まず,tryブロックの中の処理1を実行し,次にMyException型の例外オブジェクト
を投げる。すると,処理はそこで中断される。つまり,処理2は実行されない(Fig.3-(3)<2>)。
次に,MyException型の例外オブジェクトをキャッチできるcatchブロックが探されることになる。最初に見つかるのは,
2番目のcatchブロックである。そのため,2番目のcatchブロックの仮引数meが投げられた例外オブジェクトを参照するよ
うに設定し,2番目のcatchブロックの先頭から処理が再開される(Fig.3-(3)<3>)。
(2番目の)catchブロックの処理が終了すると,finallyブロックがあるので,finallyブロックに処理が移り,後処理を実行
する(Fig.3-(3)<4>)。
finallyブロックの処理を終えると,try-catch-finallyブロックの次の文からまた実行されていく(Fig.3-(3)<5>)。
次に,Fig.3-(4)の例を見てみよう。この例では,tryブロック内で例外が投げられず,処理1から処理4まですべて実行すること
になる(Fig.3-(4)<①>)。例外が投げられなかったので,tryブロック終了後,すぐにfinallyブロックに処理が移る(Fig.3-(4)<2>)。
そして,finallyブロックの処理が実行された後,try-catch-finallyブロックの次の文からまた実行されていく(Fig.3-(4)<3>)。
●キャッチされなかった例外
ところで,上図Fig.3-(5)の例を見てみよう。tryブロックの中で,MyException型オブジェクトが投げられている(Fig.3-(5)
<①>)。しかし,MyException型例外をキャッチできるcatchブロックが存在しない。しかたないので,例外オブジェクトは投げ
られっぱなしのまま,finallyブロックの内容を実行する。
問題なのは,その後である。finallyブロック内の後処理部分は実行したものの,例外はキャッチされないままの状態である。
つまり,エラー処理は行われていないのである(finallyブロックは,エラーが起こったかどうかに関係なく行われる共通の後処理部
分であって,エラー処理を担っているのではないことに注意)。
このように,例外がcatchブロックでキャッチされない状況はよく起こる。たとえば,
try{ } catch( MyException me ) { ここで,できるだけのエラー処理を行う throw me; // 再び例外を投げる }
void m( ) { try { throw new MyException( ); // ここで投げられる MyException型例外をキャッチできるcatchブロックは,このメソッド内には無い! } catch( YourException ye ) { } }
void m( ) { throw new MyException( ); // 例外射出! でもここはtryブロックの内側じゃない! }
【メソッド外への例外の伝播】
実は,メソッドの中で例外をキャッチできない場合,メソッドはその例外を,そのメソッドを呼び出しているメソッドへと投げるので
ある。これが,まさにFig.1で示したメソッド間のエラー通知である。つまり,Javaではメソッドが自分で例外を処理しきれない場合,例
外を呼び出しもとのメソッドに投げることによって,エラーを通知することができるのである。
ただし,メソッドが例外を投げるためには,メソッドがどのような型の例外を投げるかを,メソッド宣言のthrows節に加えておかなけ
ればならない。その書式を,Fig.5に示す。書き方は簡単で,( )で囲まれた仮引数リストの後ろに,予約語throwsを書いて, そのメソッド
が投げる可能性のある例外型をカンマで区切って並べるだけである。
もし,例外を投げる可能性のあるメソッドが,適切にthrows節を書いていないと,コンパイル時にエラーになる。たとえば,
class A { void m( ) throws E1 { } void h( ) { // エラー! m( ); } }
class A { void m( ) throws E1 { } void h( ) throws E1 { // OK! m( ); } }
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型を投げるかもしれない処理 }
void m( ) throws E { // E1型, E2型を投げるかもしれない処理 }
●チェックされない例外
ところで,throws節に書かなくてもコンパイルエラーにならない特殊な例外型もある。それは,Error と Runtime およびそれらのサブ
クラスである。そのためこれらは,(throws節で)チェックがされない例外と呼ばれる(Fig.2)。
Error は,Java仮想マシンで重大な実行時エラーが起こったことを表している。たとえば,クラスファイルが壊れているような場合な
どである。また,メモリ不足で新しいオブジェクトを生成することにnew演算子が失敗したときなどに投げられる OutOfMemoryError も
Error のサブクラス VirtualMachineError のサブクラスである。
こういったエラーは,予想外の場所で起こりえるし,一般のアプリケーションがエラー回復できるような種類のものではない。そのた
め,通常は,Errorおよびそのサブクラス型の例外をキャッチしようとしてはならない。こういった理由で,特にthrows節に明記する必要
は無いようになっているのである。
また,RuntimeException は,Java仮想マシンの実行中に起こる軽度のエラーで,Javaのプログラムでは,どこでも起こりえるような
エラーを表す。たとえば,
A a = null; a.m( );
【実際の使用例】
では,復習として,実際に例外を使用しているプログラム例を見てみよう(下図List 5)。List 5-<①>では,4つの例外クラス
E1〜E4を,Exceptionのサブクラスとして定義している。
クラスAの中では,処理1〜処理4を行うメソッドm1( )〜m4( )と,後処理を行うメソッドpost( )が定義されている(List 5-<2>〜<6>)。
m1( )は,フィールドiの値が1だったらE1型例外を投げる。同様に。m2( )は,iが2だったらE2型例外を投げる,という具合にm4( )まで
定義されている。
クラスExceptionExample1のメソッドh( )は,仮引数でA型メンバオブジェクトaのフィールドiを初期化している。そして,List 5-<7>
が処理の中心で,tryブロックの中で,処理1〜処理4までを実行するために,メソッドm1( )〜m4( )が順序よく呼ばれてる。
その後ろに,E1〜E4型の例外をキャッチするそれぞれのcatchブロックがあり(List 5-<8>〜<11>),最後に,finallyブロックで後処理
を行うpost( )メソッドが呼ばれている(List 5-<12>)。
main( )メソッドでは,メソッドh( )に実引数0〜4を渡して呼び出している(List 5-<13>〜<17>)。List 5-<13>の呼び出し
h( 0 );
では,例外は投げられないので,表示結果は
process #1
process #2
process #3
process #4
postprocess
----------
となり,処理1〜処理4と後処理が実行されることがわかる。List 5-<14>の呼び出し
h( 1 );
では,メソッドm1( )を呼んだときにE1型例外が投げられ,List 5-<8>のcatchブロックでキャッチされるので,表示結果は
process #1
E1
postprocess
----------
となる。実際にList 5を実行してみて,List 5-<15>〜<17>の表示結果がどうなるかも確認してみよ。
List 5 ExceptionExample1.java
【まとめ】
ここで,例外の利点をまとめてみよう。
・通常の処理の記述がシンプルで読みやすくなる
※上図List 5-<7>の部分は,正規の処理が整然と並んでいて,どのように処理が進んでいくか一目瞭然である。
これは,古典的な手法を使ったList 2-<①>の読みにくさに比べると雲泥の差である。また,エラー処理部分も,
List 2-<2>では,どの部分がどのエラーを処理しているのかひと目では分からないが,List 5-<8>〜<11>で
は,どのcacthブロックが,どの例外を処理しているかは明確にわかる。
・例外を使うと,メソッドの返り値を自由に使用することができ,例外オブジェクトが生成されて投げられるのは,
エラーが起こったときだけ
※これらの点は,メソッドの返り値をエラー通知に使用する方法に比べて,格段に優れている。
・オブジェクトという高度な情報体をエラー通知に使えるので,様々なエラー処理の方法を考えることができる
※たとえば,Javaの例外クラスが,Fig.2のように継承関係を使って系統的に組織化されているように,自分で
エラーの種類を系統的に分類することも可能である。
このように,古典的なエラー処理に比べて,例外処理は様々な利点を持っている。しかし,古典的なエラー処理(
例えば,エラーコードを返値として返すメソッドなど)にも,例外処理の仕組みに比べて単純であるという強みがある。
したがって,例外処理を主なエラー処理として使いつつ,必要に応じて古典的な方法も利用すると良いだろう。
◆JavaのAPIドキュメントでは,例外を投げる可能性があるメソッドについては,投げられる例外についても明示され
ている。JavaAPIのメソッドを利用するときには,例外についてもよく確認して,それらのメソッドから投げられる
可能性のある例外を,確実にキャッチできるようにプログラムを書くこと。
◆山崎先生による演習問題
【問題】実際にプログラムの実行時にエラーを起こしてみる。
data.txtという名前のファイルを入力用にオープンするには
FileInputStream in=new FileInputStream("data.txt") ;
で行なえる。(詳しくは入出力の回で説明する。)
ファイルが存在するかどうかは実行する時にしかわからない。
List 6 ExceptionExample2.java
【問題 1】ExceptionExample2.javaをコンパイルして実行してみよ。
このとき実行がどのような順序でされるか
行番号で答えよ。(行番号10の実行から書け)
【問題 2】次に同じフォルダにdata.txtという名前のファイルをエディターで作ってから実行してみよ。
このとき実行がどのような順序でされるか
行番号で答えよ。(行番号10の実行から書け)
List 7 ExceptionExample3.java
【問題 3】data.txtを消去せよ。ExceptionExample3.javaをコンパイルして実行してみよ。
このとき実行がどのような順序でされるか
行番号で答えよ。(行番号15の実行から書け)
【問題 4】次に行番号20の//を消してからコンパイルして実行してみよ。
実行画面に現れたメッセージを下の行から読んでいけ。
at ExceptionExample3.f(ExceptionExample3.java:7)
at ExceptionExample3.main(ExceptionExample3.java:15)
の意味を推測せよ。printStackTrace( ) メソッドはエラーの原因を見つけるのに
便利なメソッドである。
Exception 一般的例外クラス
IOException 入出力の例外クラス
FileNotFoundException ファイルが見つからないという例外クラス
この例外はそれぞれ上の例外を継承している。(下の例外程具体的な例外を表す)
List 8 ExceptionExample4.java
【問題 5】ExceptionExample4.javaをコンパイルして実行してみよ。
このとき実行がどのような順序でされるか
行番号で答えよ。(行番号10の実行から書け)
【問題 6】例外をキャッチする順序は具体的なものから先に書かれている。
これを、 catch( Exception e ) { // 一般的例外を処理する
の行から先に書くとどうなるか?
戻る