All-In-One Eclipseを使ってSampleFinallyクラスを新規に作成し、次のようなコードを入力してください。このプログラムはネットワークプログラミングでよく使われるソケットを使ったプログラムのひな型になります。Javaでソケットを表現するクラスとして用意されているjava.netパッケージのServerSocketクラスとSocketクラスを使っていますが、処理の内容はここでは考えなくても構いません。「new ServerSocket();」「ss.accept();」「s.close();」「ss.close();」のそれぞれの行で例外java.io.IOExceptionが投げられる点に注目をしてください。
import java.net.ServerSocket; import java.net.Socket; public class SampleFinally { public void execute() { ServerSocket ss = new ServerSocket(); Socket s = ss.accept(); System.out.println("accept"); // s を使った処理(省略) s.close(); ss.close(); } }
例外java.io.IOExceptionが投げられるのにキャッチをしていませんから、コンパイルエラーが発生していることを示す赤い下線が表示されます。Sample310のときと同じようにして対処しましょう。ただし、ここでは「スロー宣言の追加」を選びます。
すると、import java.io.IOException; がファイルの先頭に追加され、executeメソッドにthrows IOExceptionという宣言が追加されます(画面5)。このように、例外をキャッチせずに、そのままスローしてしまう対処方法もあります。この場合はここまでの例であったような「投げる例外クラスのインスタンスを生成する」といった処理は必要ありません。
さて、これでエラーはなくなりましたが、「new ServerSocket();」や「ss.accept();」「s.close();」でIOExceptionが発生すると、その時点で例外が投げられてしまうため、その先の処理は実行されません。これでは「ss.close();」が呼び出されないことになってしまいます。これではServerSocketを使用中の状態にしたままメソッドが終了したことになってしまいます。その結果、ServerSocketはOSに管理されているリソースを使っているので、これが解放されないことにつながります。
こういったことを防ぐためには、次のようにfinally節を使って確実にリソースが解放されるようにSocketクラスのcloseメソッドやServerSocketクラスのcloseメソッドを呼び出すようにする必要があります。
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SampleFinally { public void execute() throws IOException? { ServerSocket ss = null; try { ss = new ServerSocket(); Socket s = null; try { s = ss.accept(); System.out.println("accept"); // s を使った処理(省略) } finally { if (s != null) s.close(); } } finally { if (ss != null) ss.close(); } } }
以上のようにfinally節を使うとリソースの解放を確実に行うことができます。ただし、finally節があると、finallyブロック内の処理が必ず実行されますから、返却値があるメソッドでは若干注意が必要になります。例えば次のようなコードはどういった結果になるでしょうか。tryブロックで「return name;」としていますが、finallyブロックで「return "Finally";」としてあるので"Finally"が返りそうです。
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class Sample340 { public String execute() { BufferedReader in = new BufferedReader( new InputStreamReader(System.in)); System.out.print("Input name: "); String name; try { name = in.readLine(); return name; } catch (IOException e) { return "Exception"; } finally { return "Finally"; } } public static void main(String[] args) { Sample340 sample = new Sample340(); System.out.println(sample.execute()); } }
実際にサンプルを作って動作をさせてみましょう(画面6)。Eclipseでは「finallyブロックは正常に終了しません」といった警告が出ますがコンパイル自体は成功しています。この警告を無視して実行すると「return name;」はなかったことになり、「return "Finally";」が実行された結果が出力されます。混乱しないようにするためには「finallyブロックの中ではreturnしない」ということを基本にするのが良いでしょう。
Eclipseを使っていれば警告が表示されるので、あえて無視をしない限りこのようなコーディングをすることはないはずですが、この意味をしっかりと理解しておきましょう。
「コンパイル時にチェックされる例外」を使用するには下記の注意点も考慮する必要があります。
このように、「コンパイル時にチェックされる例外」であるjava.lang.Exceptionクラスを継承する独自の例外クラスでは強制力があることにより使いにくい面もあります。従って、場合によってはjava.lang.RuntimeExceptionクラスを継承した独自の例外クラス設計を検討する価値はあります。ただし、このクラスを使用する場合には、どのメソッドがどんな例外を投げるのかについての情報を完全にドキュメント化しておく必要があります。java.lang.Exceptionクラスのサブクラスであればコンパイラがチェックをしてくれるのですが、java.lang.RuntimeExceptionクラスのサブクラスについては、そういったチェックがされないので、ドキュメントしか頼りになるものがなくなってしまうからです。
以上のように、例外を使うとエラー処理をうまくコーディングできることが多いですから、よく理解して使えるようになってください。また、本文では説明をしませんでしたが、メソッドであればエラーコードなどを返すようにすることもできますが、コンストラクタではそういった処理をコーディングすることはできません。インスタンス生成時のエラー発生を捕捉するには例外は必須になります。
コンストラクタでのエラー捕捉には例外が必須となりますが、そうでない場合は、例外はあくまでエラーへ対応するための1つの手段でしかありません。アプリケーション全体を考えた場合は例外を使うよりも、メソッドでエラーコードを返すようにしてエラー処理をした方が良い場合もあります。どういった方法が良いのかを常に考えながらコーディングするようにしましょう。
また、例外を使うと処理の流れを変えることができますから、条件文のような使い方も可能です。しかし、例外はその名のとおり、「例外的な処理が発生したことを通知する、その通知を捕捉する」という目的で設計されています。処理の流れを変えるために例外を使うというのには何のメリットもありませんから、目的以外で使用することがないようにしましょう。
こういった点に注意しながら、例外処理の仕組みをよく理解したうえで有効に利用できるようになってください。次回は、型について解説をする予定です。お楽しみに。
小山博史(こやま ひろし)
情報家電、コンピュータと教育の研究に従事する傍ら、オープンソースソフトウェア、Java技術の普及のための活動を行っている。Ja-Jakartaプロジェクト(http://www.jajakarta.org/)へ参加し、コミッタの一員として活動を支えている。また、長野県の地域コミュニティである、SSS(G)(http://www.sssg.org/)やbugs(J)(http://www.bugs.jp/)の活動へも参加している。
Copyright © ITmedia, Inc. All Rights Reserved.