Javaの例外処理で知らないと損する7つのテクニック:【改訂版】Eclipseではじめるプログラミング(24)(2/3 ページ)
これからプログラミングを学習したい方、Javaは難しそうでとっつきづらいという方のためのJavaプログラミング超入門連載です。最新のEclipseとJava 6を使い大幅に情報量を増やした、連載「Eclipseではじめるプログラミング」の改訂版となります(この回と前回のみ、別連載「EclipseでJavaに強くなる」の改訂版です。今回は第4回Javaの例外のテクニックを知る」の改訂版です)
【3】独自の例外に置き換えてthrowするには
独自ライブラリを提供する場合は、メソッド内で発生する複数の例外を独自ライブラリの例外へ置き換えて投げるように設計することがあります。
例えば、ログインクラスライブラリを独自に作成した場合は、「ログインに失敗しました」といった例外を投げるように設計したとします。内部で使っているファイルシステムやデータベース管理システムでは、読み込み権限がないとか接続ができなかったとか、そういったエラーが発生しますが、そういった細かい例外まで、このライブラリを使う側が意識しなくても済むようにするわけです。
何が原因で例外が発生したのかを把握できるように
しかし、実際に問題を解決するときには、何らかのヒントが欲しいため、何が原因で例外が発生したのかを把握できるようにしておきたいことがあります。そんなときは、次のSampleException3のように「java.lang.Throwable」を引数に持つコンストラクタを用意します。もともとの原因をたどれるようにするのです。
package sample24; public class SampleException3 extends Exception { private static final long serialVersionUID = 1L; public SampleException3(Throwable cause) { super(cause); } }
次にSampleException3クラスの動作を確認して理解するために、SampleUserList2クラスを作成してみましょう。
package sample24; public class SampleUserList2 { private SampleUserList sample = new SampleUserList(); public boolean login(String name, String password) throws SampleException3 { try { return sample.login(name, password); } catch (SampleException e) { throw new SampleException3(e); } catch (SampleException2 e) { throw new SampleException3(e); } } }
このサンプルでは、SampleUserListクラスのloginメソッドで発生した例外をすべてSampleException3クラスの例外に置き換えています。ここで、例外の発生原因となった情報はSampleException3クラスのコンストラクタへパラメータとして渡しているので、SampleUserList2クラスが投げる例外をキャッチするメソッド側でも例外の発生原因を把握できます。
実行プログラム
どのように把握できるかを見るために、SampleUserList2クラスを使うプログラムを作成してみましょう。例えば、次のSampleApp2クラスのようなプログラムで確認できます。これはSampleAppクラスと同様の処理をするプログラムですが、SampleUserList2が投げる例外はSampleUserListが投げる例外とは違うため、出力結果は変わります。
package sample24; public class SampleApp2 { public static void main(String[] args) { String[] users = { "Sara", "Akira", "Akira", "Jacky", " " }; String[] passwords = { "? ", "777", "111", "abc", "xyz" }; SampleUserList2 sample = new SampleUserList2(); // SampleUserList2クラスを使用 for (int i = 0; i < users.length; i++) { try { System.err.print("i=" + i + ":"); boolean isLogin = sample.login(users[i], passwords[i]); if (isLogin) { System.err.println("ログインに成功しました。(" + users[i] + ")"); } else { System.err.println("ログインに失敗しました。"); } } catch (SampleException3 e) { // 例外のキャッチはSampleException3だけ e.printStackTrace(); } System.err.println(""); } } }
サンプルの実行結果
このプログラムを実行してみると、SampleException3の例外が投げられたことに加えて、何が原因で例外が発生したのかも確認できます(画面3)。
【4】finallyでリソースの解放を確実に行うには
Eclipseを使ってSampleFinallyクラスを新規に作成し、次のようなコードを入力してください。
package sample24; 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でソケットを表現するクラスとして用意されているjava.netパッケージのjava.net.ServerSocketクラスとjava.net.Socketクラスを使っていますが、処理の内容は、ここでは考えなくても構いません。「new ServerSocket();」「ss.accept();」「s.close();」「ss.close();」のそれぞれの行で例外「java.io.IOExcepiton」が投げられる点に注目してください。
Eclipseで[スロー宣言の追加]
例外java.io.IOExceptionが投げられるのにキャッチをしていませんから、コンパイルエラーが発生していることを示す赤い下線が表示されます。Eclipseの機能を使って対処しましょう。ただし、ここでは[スロー宣言の追加]を選びます。
すると、IOException、Exception、Throwableの選択肢が表示される(画面5)ので、IOExceptionを指定します。この結果、「import java.io.IOException;」がファイルの先頭に追加され、executeメソッドに「throws IOException」という宣言が追加されます。
このように、例外をキャッチせずに、そのままスローしてしまう対処方法もあります。この場合はここまでの例であったような「投げる例外クラスのインスタンスを生成する」といった処理は必要ありません。
finally節を使って確実にリソースを解放する
さて、これでエラーはなくなりましたが、「new ServerSocket();」や「ss.accept();」「s.close();」でIOExceptionが発生すると、その時点で例外が投げられてしまうため、その先の処理は実行されません。これでは「ss.close();」が呼び出されないことになり、ServerSocketを使用中の状態にしたままメソッドが終了したことになってしまいます。
ここで、ServerSocketはOSに管理されているリソースを使っています。つまり、OSに管理されているリソースについて、ServerSocketを使用中の状態にしたままとなるので、「リソースが解放されない」ことにつながります。
こういったことを防ぐためには、次のようにfinally節を使って確実にリソースが解放されるようにSocketクラスのcloseメソッドやServerSocketクラスのcloseメソッドを呼び出すようにする必要があります。
package sample24; 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ブロックの中ではreturnしない
ただし、finally節があると、finallyブロック内の処理が必ず実行されますから、返却値があるメソッドでは若干注意が必要です。
例えば、次のようなコードはどういった結果になるでしょうか。tryブロックで「return name;」としていますから正常処理をしたときはnameを返却したいのでしょうが、finallyブロックで「return "Finally";」としてあるので、"Finally"が返りそうです。
package sample24; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; public class SampleFinally2 { 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) { SampleFinally2 sample = new SampleFinally2(); System.out.println(sample.execute()); }
実際にサンプルを作って動作をさせてみましょう(画面6)。
Eclipseでは「finallyブロックは正常に終了しません」といった警告が出ますがコンパイル自体は成功しています。この警告を無視して実行すると「return name;」はなかったことになり、「return "Finally";」が実行された結果が出力されます。そもそも警告を無視しているのもいけませんが、混乱しないようにするためには「finallyブロックの中ではreturnしない」ということを基本にするのがいいでしょう。
Copyright © ITmedia, Inc. All Rights Reserved.