例外処理

通常、メソッドが終了すると戻り値とともに、呼び出し側に制御が返されます。 しかし、何らかの例外的な状況が発生して、メソッドが異常終了すると、通常とは異なる形で呼び出し側に制御が返されます。 本章では、ファイルの読み書きを題材に、この例外的な状況の取り扱い方法を説明します。

ファイルの読み書き

java.io パッケージには、入出力処理に関係するクラスが多数含まれています。 この中には、テキスト・ファイルの内容を読み込んだり、新しい内容をファイルに書き込んだり (保存したり) するためのクラスも含まれます。

ファイルの読み込み

テキスト・ファイルの内容を読み込むには、java.io.FileReader クラスと java.io.BufferedReader クラスを使います。 例として、まず ReadExample.java というテキスト・ファイルの内容を読み込んで画面に表示するプログラムを示します。

このプログラムでは、まず FileReader オブジェクトを作ります。 コンストラクタの引数は、読み込みたいテキスト・ファイルの名前です。 このオブジェクトは、ファイルの先頭から順に 1 文字ずつ内容を読み込む機能を提供します。

次に、この FileReader オブジェクトをコンストラクタの引数にして、BufferedReader オブジェクトを作ります。 このオブジェクトは、引数で渡された FileReader オブジェクトを経由してファイルの内容を 1 文字ずつ読み込みます。 また、このオブジェクトは、FileReader オブジェクトが読み込んだ文字をバッファリング (buffering) することで、効率よくファイルの内容を読み込んだり、1 行単位で内容を読み込めるようにします。

ファイルの内容を 1 行分読み込むには、BufferedReader オブジェクトの readLine メソッドを呼びます。 このメソッドは、呼ばれるたびにファイルの先頭から 1 行ずつ順に読み込んで、文字列 (String オブジェクト) として返します。 ファイルの最後の行を読み込んでしまって、それ以上、読み込める行がなくなると、readLine メソッドは null を戻り値として返します。

上の例で、readLine メソッドを呼んでいる式

の意味は次のとおりです。 まず変数 reader が表すオブジェクトの readLine メソッドを呼び、その戻り値を 変数 line に代入します。 この代入式は全体が括弧で囲まれていますが、この計算結果の値は変数 line に代入された値です。 次に、この代入式の計算結果を定数 null と比べます。 もし 2 つの値が異なれば true、一致すれば false が、この式全体の計算結果となります。

結局、上の例の while 文は、ファイルから 1 行ずつ読み込んでゆき、全ての行を読み込み終わったら終了する、ということになります。 各回の繰り返しのたびに読み込まれる行は、変数 line に代入され、変数 line に代入された文字列は、println メソッドにより画面に表示されます。 このため、while 文の実行が終わると、ファイルの内容が全てが画面に表示されているはずです。

ファイルの読み込みが終了したら、最後に close メソッドを呼びます。 close メソッドは、忘れずに必ず呼ばなければなりません。 これを忘れると、オペレーティング・システムがファイルの読み込みに必要な情報をいつまでもメモリ中に保持してしまい、結果的にプログラムの実行性能に悪影響がでることがあります。

main メソッドは、以上のようにファイルの読み込みをおこないますが、FileReader オブジェクトや BufferedReader オブジェクトは、何らかの理由でファイルの読み込みに失敗すると、例外的な状況が発生したとしてプログラムの実行を異常停止させてしまいます。 上のプログラムでは、そのような場合、プログラム全体が異常終了するようになっています。 それを指定しているのが、

という行の throws IOException の部分です。 詳しくは後に説明します。

単語の読み込み

上の例では readLine を使って 1 行ずつファイルの内容を読み込みました。 しかし、行単位でなく、単語単位でファイルの内容を読む込むことも可能です。 行を単語に分解するために java.util.StringTokenizer というクラスが標準で用意されていますが、ここではこのクラスを使わずに、単語単位でファイルの内容を読み込むプログラムを書いてみましょう。

main メソッドは先の例とほぼ同じで、実際の処理は readWord メソッドがおこないます。 このメソッドは BufferedReader オブジェクトを引数として受け取り、単語を 1 つ読み込んで戻り値として返します。 readWord メソッドを繰り返し呼ぶと、ファイルの先頭からひとつずつ単語が読み込まれ、戻り値として返されます。 ファイルの末尾に達して、それ以上単語がないときは、null を戻り値として返します。

readWord メソッドは、BufferedReader オブジェクトを使ってファイルの先頭から 1 字ずつ読み込みます。 このメソッドは、まず、文字や数字があらわれるまで空白や記号を読み飛ばします。 文字や数字があらわれたら 1 字ずつ順に読み込みます。 そして、空白や記号があらわれたら、そこで終了し、それまでの間に読み込んだ文字をまとめて文字列として戻り値とします。 以上の動作は、下のようなオートマトン (automaton) を使って表現することができます。

丸がオートマトンの状態を、矢印が入力に対してどのように状態が変わるかを表します。 入力というのは、この場合、ファイルから読み込んだ 1 字です。 S とあるのが初期状態、E とあるのが終了状態です。 まず状態 S では、ファイルから 1 字ずつ読み込んでゆき、文字や数字を読み込んだら、状態 1 へ遷移します。 状態 1 で読み込んだ文字や数字は単語の一部になります。 オートマトンがこの状態にあるときに、空白や記号を読み込んだら、状態 E へ遷移して終了します。 また、オートマトンが状態 S や E であるときに、ファイルの末尾に達してそれ以上読み込めなくなったら、やはり状態 E へ遷移して終了します。

readWord メソッドは、このオートマトンをプログラムにしたものといえます。 まず最初に StringBuffer オブジェクト (正確には java.lang.StringBuffer オブジェクトですが、java.lang は常に省略できます) がでてきますが、これは文字を集めて文字列を作るためのオブジェクトです。 append メソッドによって、先頭から順に 1 文字ずつ追加してゆき、最後に toString メソッドを呼ぶと、全体をまとめて文字列にしたものが戻り値として返されます。

最初の do 文はオートマトンの状態 S に対応します。

BufferedReader オブジェクトの read メソッドを呼ぶと、ファイルの先頭から順に 1 字ずつ読み込むことができます。 read メソッドの戻り値は int 型です。 本来は読み込んだ文字を返すので戻り値は char 型 (読み込んだ文字に対応する文字コード番号) のはずですが、char 型は正の整数しか表せません。 read メソッドは、ファイルの末尾に達して読み込む文字がない場合、-1 を返します。 このため、-1 も表せる int 型が read の戻り値の型になっています。

変数 c に代入された read の戻り値が -1 のときは、readWord メソッドは即座に終了して、null を戻り値として返します。 一方、変数 c が文字や数字ではない場合、do 文の先頭に戻って処理を繰り返します。 それ以外の場合は do 文を終了して、状態 1 に対応する次の do 文に移ります。

変数 c が文字や数字であるか否かを判定するのに java.lang.Character クラスの static メソッドである isLetterOrDigit を呼んでいます。 このメソッドに変数 c の値を引数として渡すとき、char 型にキャストしていますが、これは変数 c の型が int 型であるのにもかかわらず、このメソッドの引数の型は char 型であるためです。 int 型も char 型も整数を表す型ですが、int 型の方が表せる整数の範囲が広いので、このようにキャスト演算が必要になります。

次の do 文は状態 1 に対応します。

状態 1 で読み込んだ文字は単語の一部になります。 そこで、まず StringBuffer オブジェクトの append メソッドを呼び、変数 c が表す文字を記録します。 次に新しい文字を読み込み、文字または数字であるか否かを判定します。 そうである場合は do 文の先頭に戻って処理を繰り返します。 これはオートマトンで状態 1 から出て状態 1 へ向かう矢印に対応します。 文字や数字以外を読み込んだ場合や、ファイルの末尾に達してそれ以上読み込めない場合は do 文を終了し、StringBuffer オブジェクトの toString メソッドを呼んで、これまで記録した文字を文字列として取り出します。 readWord メソッドの戻り値は、この文字列です。

変数 c の値を append に引数として渡すときにも char 型へキャストしていますが、これは append がオーバーロードされているからです。 char 型の引数をとる append は、引数として渡された文字をそのまま記録します。 一方、int 型の引数をとる append は、引数として渡された整数を文字列にして記録します。 例えば

このプログラムを実行すると表示されるのは 65A です。 変数 i の値は 65 ですから、append(i) により文字列 "65" が記録されます。 一方、append((char)i) では文字コード番号 65 に対応する文字 A が記録されます。 したがって変数 s が表す文字列は 2 つを連結した "65A" となります。

ファイルへの書き込み

テキスト・ファイルに文字列を書き込むには、java.io.FileWriter クラスと java.io.BufferedWriter クラス、java.io.PrintWriter クラスを使います。 例として output.txt というファイルに文字列を書き込むプログラムを示します。

このプログラムを実行すると、output.txt というファイルに

と書き込まれます。 もし output.txt というファイルが既に存在していると、まずファイルの中身が全て消去されてから、上の内容が書き込まれます。

ファイルへの書き込みを直接担当するオブジェクトは FileWriter オブジェクトです。 この他に BufferedWriter オブジェクトと PrintWriter オブジェクトがありますが、これらはコンストラクタの引数で与えられたオブジェクトをくるんで、付加的な機能を与えます。 例えば BufferedWriter オブジェクトは、細切れの書き込みをまとめて、効率よくファイルへの書き込みができるように、書き込みの調整をおこないます。 PrintWriter オブジェクトは、println や print という便利なメソッドを提供します。 これらのメソッドはオーバーロードされており、引数として渡された値を適当な文字列に変換して、ファイルに書き込みます。 引数が int 型の場合は、int 型の整数値を文字列にして書き込まれます。 なお print メソッドは渡された値を書き込むだけですが、println メソッドは渡された値を書き込んだ後、改行文字も書き込みます。

ファイルの読み込みのときと同様、ファイルへの書き込みが全て終了したら、PrintWriter オブジェクトの close メソッドを呼ばなければなりません。 これを忘れると、ファイルへの書き込みが完全には終了せず、最後に書き込んだはずの文字列が一部書き込まれないままになってしまう可能性もあります。

ファイルの存在確認

FileWriter オブジェクトは、書き込み先のファイルが既に存在していると、最初にそのファイルの内容を全て消去してしまいます。 勝手に内容を全て消去されては困る場合は、事前に書き込み先のファイルが既に存在するか否かを確かめる必要があります。

次に示すのはコマンド行引数のファイルが存在すれば true を、そうでなければ false を表示するプログラムです。

java.io.File クラスのコンストラクタは、ファイルのパス名を引数にとります。 このクラスのオブジェクトの exists メソッドを呼ぶと、そのパス名のファイルが存在すれば true、そうでなければ false が返ります。

例外

ファイルの入出力をおこなうプログラムを書くときは、例外処理についても考慮しなければなりません。 上の例では、全てのメソッドに

と書くことで、対処してきました。

ファイルの入出力のような処理は、外部のハードウェアが関係するので、予期しない原因で処理に失敗することがあります。 例えば、実行中に抜き差し可能な外付けのディスクに、ファイルを保存しているとします。 すると、そのファイルに書き込んでいる最中に、突然、ディスクがはずされて書き込みが失敗する可能性があります。 そのような事は滅多におこりませんが、万一おきたときには、適切な処置をおこなう必要があります。 実際、コンピュータ・システムへの不正な侵入は、多くのプログラムがこのような異常事態に正しく対処していないことを利用します。 悪意ある侵入者は、わざと異常事態を引き起こしてプログラムを誤動作させ、システムへ進入するのです。

しかしながら、例外処理の機能を使わずに、このような滅多におきない事態に対処しようとするとプログラムが煩雑になってしまいます。 例えば先の WriteExample の例を使って考えてみます。

実は、このプログラムは異常事態に対応していない、少々いい加減なプログラムです。 本当は print メソッドや println メソッドが正しく処理を実行できたか否か、checkError メソッドを呼んで確かめなければなりません。 次に毎回、checkError メソッドを呼ぶように書きかえた WriteExample クラスの定義を示します。

print メソッドや println メソッドを呼ぶたびに、戻り値を調べる if 文を書かなければなりません。 checkError メソッドは、過去に一度でも異常事態が発生して PrintWriter オブジェクトが書き込み処理に失敗すると、以後 true を返します。

このように、メソッドを呼ぶたびに異常事態の発生の有無を検査するのは、非常に面倒で、プログラムも読みにくくなります。 異常事態は基本的に滅多におこらないことなので、これだけの手間をかけるのを惜しみ、検査を省略する場合も少なくありません。 上の例の場合も、毎回検査するのではなく、最後の println の呼び出しのときにだけ検査をし、それまでの print メソッドや println メソッドの呼び出しが正しく実行できたか、まとめて検査するように省略してしまうかもしれません。 しかし最悪の場合、そのような省略の結果としてコンピュータ・システムへの不正侵入の足がかりを作ってしまうこともあります。

throw

この問題を解決するのが例外処理です。 一般にメソッドは return 文によって終了しますが、throw 文で終わることもできます。 throw 文は次のような形をとります。

return 文とまったく同じですが、式の計算結果は Throwable (正確には java.lang.Throwable) オブジェクトか、Throwable のサブクラスのオブジェクトでなければなりません。 また return 文と異なり、throw に続く式を省略することはできません。

return 文はメソッドの実行が正常に終了したときに使いますが、throw 文は何らかの例外的な状況が発生して、メソッドの実行が異常終了したときに使います。 return 文でいう戻り値に対応するのが Throwable オブジェクトで、このオブジェクトのことを例外と呼びます。

throw 文によって例外が投げられると、そのメソッドの実行は終了し、その例外が呼び出し側のメソッドに投げ返されます。 return 文によって戻り値が返されるのではなく、throw 文によって例外が投げ返されると、通常、呼び出し側のメソッドも連鎖的に異常終了し、投げ返された例外を、そのメソッドを呼び出したメソッドに、さらに投げ返します。 こうして、呼び出されたメソッドから呼び出したメソッドへと、連鎖的に例外を投げ返しながら、各メソッドが異常終了してゆき、最終的に一番最初に呼び出された main メソッドが異常終了します。

次のプログラムは throw 文の使い方の例です。

コマンド行引数をつけずに、このプログラムを実行すると、throw 文が実行されてプログラム全体の実行が異常終了し、次のようなメッセージが表示されます (Java 言語の処理系によってメッセージの内容の詳細は異なります)。

このメッセージは、ThrowTest.java の 4 行目で例外 (java.lang.Throwable オブジェクト) が投げられてプログラムの実行が終了した、という意味です。 最初に例外を投げたのは message メソッドで、その後 main メソッドが連鎖的に異常終了したことがわかります。

このように通常、例外が投げられるとプログラム全体が終了することになります。 return 文では、return 文を含むメソッドの実行が終了するだけで、それを呼び出した側のメソッドの実行は再開されますから、これは大きな違いです。

例外的な状況が発生してメソッドが異常終了したら、プログラム全体も終了させるという方針は、一見乱暴ですが、安全な方針といえます。 むしろ、一部のメソッドが異常終了して、正常に処理を完了していないのに、無視してプログラムを続行する方が乱暴です。 実際、そのように続行すると、先に述べたように、不正侵入の足がかりにされてしまう可能性もあります。

throws

メソッドが例外を投げて異常終了する可能性があるときは、その旨をメソッドの定義に書いておかなければなりません。 その場合、冒頭の引数列の後ろに throws と書き、それに続けて、メソッドが投げる可能性のある例外オブジェクトのクラスの名前を書きます。 throws 以下をメソッドの throws 節といいます。

例えば、上の例の message メソッドの場合、Throwable オブジェクトを投げる可能性があるので、

引数列の後に throws Throwable と throws 節を書きます。 throws の最後には s がつくことに注意してください。

throws 節を書かなければならないのは、throw 文を含むメソッドだけではありません。 例外を投げて異常終了するかもしれないメソッドを呼び出しているメソッドも、連鎖的に異常終了して例外を投げる可能性がありますから、throws 節を書かなければなりません。 上の例では、main メソッドがこれに当てはまります。

例外を投げるかもしれないメソッドの定義にいちいち throws 節を書くのは面倒ですが、一方で、そのメソッドがどのような理由で異常終了する可能性があるかが明確になります。 明確にしておけば、異常終了からの回復処理を、後で説明する try 文を使って記述するときに、正しい回復処理を書く助けになります。

throws 節を誤りなく書くことは大切なので、コンパイラも、本来書かなければいけない throws 節が誤って抜けていたり、throws 節のクラス名が間違っていないか、コンパイル時に検査します。 したがって入出力処理のように、例外を投げて異常終了するかもしれないメソッドをプログラムの中で利用するときは、各メソッドの定義に正しく throws 節を書いておかないと、コンパイルすることができなくなります。 このため、本章の冒頭に示したファイルの読み書きをするプログラムでは、全てのメソッドに

と書いていました (import 宣言があれば java.io は省略可能です)。 ファイルの読み書きに関連するメソッドが投げる例外は、ほとんどが java.io.IOException オブジェクトですから、このように throws 節を書いておけば、多くの場合十分です。

メソッドが投げる可能性のある例外のクラスが複数あるときは、カンマ , で区切って全て並べなければなりません。 例えば java.io.IOException の他に java.lang.ClassNotFoundException オブジェクトを例外として投げる可能性があるとします。 その場合、throws 節は

と書かなければなりません。 java.lang は常に省略できるので、この例でも省略しました。

throws 節に書かれたクラスにサブクラスがある場合、それらのサブクラスについては throws 節に書かずに省略することも可能です。 例えば java.io.FileReader クラスのコンストラクタは java.io.FileNotFoundException を例外として投げる可能性がありますが、この例外のクラスは java.io.IOException のサブクラスです。 したがって、

は以下のように

と省略して書いても同じことです。

さまざまな例外のクラス

throw 文で投げられる例外は、java.lang.Throwable クラスかそのサブクラスのオブジェクトだけですが、java.lang.Throwable にはいくつか主要なサブクラスがあります。

java.lang.RuntimeException のサブクラスには、明示的に throw 文で投げなくても、暗黙のうちに投げられる可能性のある例外のクラスが含まれます。 例えば

などが含まれます。

java.lang.NullPointerException は、メソッドやフィールドを呼び出そうとしたとき、対象となるオブジェクトが null であると投げられる例外のクラスです。

このプログラムを実行すると、変数 fileName が表すオブジェクトの equals メソッドを呼ぼうとしたときに、例外 java.lang.NullPointerException が投げられます。 変数 fileName の値は null で、メソッドの呼び出しができないからです。

main メソッドに throws 節がないことに注意してください。 java.lang.NullPointerException は java.lang.RuntimeException のサブクラスなので、throws 節に書く必要がありません。

java.lang.ClassCastException はキャスト演算に失敗したときに投げられる例外です。 例えば

このプログラムを実行すると、Point3D クラスの main メソッドの中の最後で、変数 p の値を Point3D 型にキャストしようとしたとき、例外 java.lang.ClassCastException が投げられます。 変数 p が表すオブジェクトは、Point3D オブジェクトではないからです。 このキャスト演算は、変数 p の型は Point だけれども、変数 p が実際に表しているのが Point3D (またはそのサブクラスの) オブジェクトであった場合にのみ、成功します。

最後の java.lang.ArrayIndexOutOfBoundsException は、存在しない配列の要素を使用しようとしたときに投げられる例外です。 配列の要素を指定するときにインデックスを与えますが、この値が負の値であったり、あるいはその配列の大きさよりも大きい値であると、この例外が投げられます。

配列 integers の大きさは 3 で、有効なインデックスの値は 0 から 2 までなので、インデックスに 3 を指定すると例外が投げられます。

java.lang.RuntimeException のサブクラスの例外は、このように、プログラムのいたるところで投げられる可能性があります。 そのため、いちいち throws 節に書くことにすると、ほとんど全てのメソッドの throws 節に書かなければならなくなります。 このため、これらのクラスの例外は throws 節に書かなくてもよいことになっているのです。

try 文

throw 文で例外が投げられると、メソッドが連鎖的に終了して、やがてはプログラム全体が終了してしまいますが、try 文を使うとこの連鎖を途中で止めることができます。 try 文は次のような形をとります。

ブロックとは、複数の文の並びを波括弧 {} で囲んだものです。 try に続くブロックのことを try ブロック、catch からそれに続くブロックを catch 節といいます。 catch 節のことを例外ハンドラ (handler) ということもあります。

catch 節では、異常終了を引き起こしている状況からの回復処理をおこないます。 プログラムを正常に実行できる状態へ回復させ、処理を再開させるのです。 例として、以下に try 文を使ってファイルに書き込みをおこなうプログラムを示します。

この例では、BufferedWriter オブジェクトを使ってファイルに書き込みをおこないます。 このオブジェクトの write メソッドを呼ぶと、引数の文字列がファイルに書き込まれ、newLine メソッドを呼ぶと、改行文字がファイルに書き込まれます。

try 文の実行が始まると、まず try ブロックの中の文が順に実行されます。 try ブロックの実行中に例外が投げられることがなければ、try ブロックの実行終了とともに try 文全体の実行も終了します。 catch 節はまったく実行されません。 しかしながら BufferedWriter クラスの write メソッドや newLine メソッドなどは、例外 java.io.IOException を投げて異常終了することがあります。 もし try ブロックの中で例外が投げられると、try ブロックの実行はそこで打ち切られ、残りの部分は実行されません。

try ブロックの中で例外が投げられると、catch 節がその例外を捕まえるか否か調べられます。 もし捕まえられなかった場合は、try 文がない場合と同じです。 try 文が書かれているメソッドの実行はそこで異常終了し、投げられた例外を、そのメソッドの呼び出し側へ投げ返します。 上の例では main メソッド全体が連鎖的に異常終了し、投げられた例外を呼び出し側に投げ返します。

catch 節は、例外が catch 節に指定されたクラスのオブジェクトか、あるいはそのサブクラスのオブジェクトであるときに、その例外を捕まえます。 上の例では、例外が java.io.IOException クラス (java.io は import 宣言があるので省略されています) またはそのサブクラスのオブジェクトであれば、catch 節によって捕まえられます。 catch 節が例外を捕まえると、捕まえた例外を引数に、catch 節のブロックが実行されます。 引数のスコープ (有効範囲) は catch 節のブロックの中だけです。 上の例では、捕まえた例外は引数 e となり、catch 節のブロックが実行されます。

catch 節のブロックの実行が終了すると、try 文の実行全体が正常終了したことになり、try 文の次の文が (もしあれば) 実行されます。 それ以上、メソッドが連鎖的に異常終了して、例外が呼び出し側へと投げ返されることはありません。 したがって上の例でも、main メソッドには throws 節がありません。

catch 節のブロックの中には、異常終了を引き起こしている状況からの回復処理を書きます。 何を書いてもよいのですが、catch 節の実行後はプログラムの実行が正常状態へ戻るので、捕まえた例外を何もせずに無視するようなことを書いてはいけません。 それでは何のために例外処理の機能を Java 言語が提供しているかわかりません。 もっとも避けるべきなのは、次のような空の catch 節です。

このような catch 節を書くぐらいなら、例外が投げられたときにはプログラム全体が異常終了するようにした方がよいでしょう。

最後に try ブロックの中で例外が投げられたときの流れを確認するため、下のような例を考えましょう。

このプログラムをコマンド行引数なしで実行すると、例外が投げられますが、

と表示して正常終了します。 3 の前に 2 が表示されないことに注意してください。

複数の catch 節

いくつもの種類の例外に対応するため、ひとつの try ブロックに複数の catch 節を書くことができます。 try ブロックの中で例外が投げられると、catch 節を上から順に調べていって、投げられた例外のクラスと一致した最初の catch 節が選ばれて、その catch 節のブロックが実行されます。 例えば

FileReader クラスのコンストラクタは例外 java.io.FileNotFoundException を投げる可能性があり、また BufferedReader クラスの readLine メソッドは java.io.IOException を投げる可能性があります。 投げられた例外が java.io.FileNotFoundException であれば上の catch 節が、java.io.IOException であれば下の catch 節が選ばれて実行されます。

実は java.io.FileNotFoundException クラスは java.io.IOException のサブクラスなので、下の catch 節は例外がどちらのクラスのオブジェクトでも捕まえることができます。 しかし、catch 節は上から順に一致するか調べられるので、投げられた例外が java.io.FileNotFoundException であれば、上の catch 節が先に一致し、それが選ばれて実行されます。 下の catch 節は実行されません。

finally ブロック

try 文には finally ブロックを加えることもできます。 finally ブロックは、try ブロックの中で例外が投げられても、投げられなくても、catch 節が実行されても、されなくても、どのような場合でも、try 文が終了する直前に実行されます。

実は先に示した WriteExample2 クラスは小さな誤りを含んでいました。 例外が投げられたときは、BufferedWriter オブジェクトの close メソッドが呼ばれずにプログラムが終了します。 close メソッドは必ず最後に呼ばれなければならないので、これでは正しいプログラムとはいえません。 そこで finally ブロックを使って、必ず最後に close メソッドが呼ばれるように修正しましょう。

catch 節の次の finally というキーワードと、それに続く波括弧で囲まれたブロックが finally ブロックです。

FileWriter クラスのコンストラクタは例外を投げる可能性があります。 もし例外が投げられると、BufferedWriter オブジェクトが作られずに try ブロックの実行が異常終了してしまいますから、変数 writer の値は null のままです。 したがって finally ブロックの中では、変数 writer の値が null か否かを調べ、null でないときだけ close メソッドを呼びます。 null の場合は BufferedWriter オブジェクトがまだ作られていないので、close メソッドを呼ぶ必要はありません。

修正したプログラムでは、main メソッドに throws 節がついていることに注意してください。 finally ブロックの中で close メソッドを呼んでいますが、このメソッドも異常終了して例外 java.io.IOException を投げる可能性があります。 もし例外を投げると、try 文全体が異常終了し、main メソッド自体も連鎖的に異常終了します。 したがって main メソッドには throws 節が必要なのです。

finally ブロックや catch 節の中で投げられた例外は、 同じ try 文の catch 節で捕まることなく、try 文の外へそのまま投げられます。 どうしても投げられた例外を捕まえたいときは、その try 文全体を別な try 文の try ブロックの中に入れ子になるように書き、外側の try 文の catch 節で捕まえる必要があります。 つまり、

のように書けば、内側の try 文の finally ブロックで投げられた例外を、外側の try 文の catch 節で捕まえることができます。

try ブロックと finally ブロックだけで catch 節がない try 文を書くこともできます。 その場合、try ブロックが例外を投げずに正常終了した場合も、途中で例外を投げて異常終了した場合も、最後に必ず finally ブロックが実行されます。 try ブロックが例外を投げている場合は、finally ブロックの終了後、try 文全体は異常終了し、投げられた例外は、呼び出された側のメソッドから、呼び出した側のメソッドへと、連鎖的に投げ返されてゆきます。


Copyright (C) 2003 by Shigeru Chiba, All rights reserved.