ところで、finallyブロックの中でtry文を記述するとネストが深くなって気になる人もいるかもしれません。そのようなときは、「in.close();」のエラーをEclipseで修正するときに、[スロー宣言の追加]を選んでマウスで左クリックしてください。
すると、メソッドに「throws IOException」が追加されます。mainメソッドにつけてしまうと、例外をcatchできなくなるので、次のSample02クラスのようなプログラムにすると、その例外をcatchできるようになります。詳細については、次回の独自の例外定義について説明するときにしますが、このようにすることで、try文が深くネストすることを防げます。
package sample23; public class Sample02 { public static void main(String[] args) { Sample02 App app = new Sample02(); try { app.exec(); } catch (IOException e) { System.err.println("in.close()でIOExceptionが発生"); } } public void exec() throws IOException { java.io.BufferedReader in = new java.io.BufferedReader( new java.io.InputStreamReader(System.in)); System.out.print("Input name:"); String name; try { name = in.readLine(); System.out.println("Hello, "+name); } catch (java.io.IOException e) { System.err.println("in.readLine()でIOExceptionが発生"); } finally { in.close(); } } }
実行時に発生する例外はプログラミングエラーに対して使用されます。この例外はJavaではjava.lang.RuntimeExceptionクラスで表現されています。
このサブクラスには、パラメータが不正であることを意味する「java.lang.IllegalArgumentException」や配列の添え字が範囲外であることを意味する「java.lang.IndexOutOfBoundsException」や、メソッドを呼び出そうとした変数がnullを参照していたことを意味する「java.lang.NullPointerException」(通称「ヌルポ」)といったものがあります。
実行時に発生する例外のほとんどは、メソッドのAPI仕様に従わずにメソッド呼び出しを行うこと(事前条件違反、precondition violation)が原因です。例えば、次のプログラムはコンパイルエラーにはなりませんが、実行するとエラーが発生します。
package sample23; public class Sample03 { public static void main(String[] args) { int[] a = new int[4]; int i = 3; System.out.println("a[4]:"+a[i+1]); } }
配列「a」の要素へアクセスするためにはインデックスへは0から「a.length-1」の間の整数値しか使用できないという事前条件に対して違反をし、配列に存在しない要素へアクセスしようとしたためにエラーとなってしまうのです。
ここで次のように、java.lang.ArrayIndexOutOfBoundsExceptionもキャッチできますが、本来は「System.out.println("a[4]:"+a[i+1]);」を無条件に実行するコーディングをしているのはプログラマのミスですから、このようなjava.lang.RuntimeExceptionを継承した例外のキャッチをするコーディングは推奨されません。条件文で「i+1」が4未満である場合に「System.out.println("a[4]:"+a[i+1]);」を実行するようにコードを修正するべきです。
package sample23; // 例外のcatchの悪い例 public class Sample04 { public static void main(String[] args) { int[] a = new int[4]; int i = 3; try { System.out.println("a[4]:"+a[i+1]); } catch (java.lang.ArrayIndexOutOfBoundsException e) { System.out.println("エラー"); } } }
このように、「実行時に発生する例外」はプログラマのミスでエラーが発生しているということを教えてくれます。実際に出合ったときにはtry文を使って対処するのではなく、コードを修正して対応するということが基本になります。
JavaVMが実行不能になるようなエラーが発生した場合にはjava.lang.Errorを継承したクラスが使われます。「java.lang.VirtualMachineError」などがあります。
こういったエラーが発生するのはシステムが異常な状態になったときだけなので、通常のアプリケーションではキャッチするべきではありません。このため、「実行時に発生する例外」と同様にjava.lang.Errorについてはキャッチしなくてもコンパイル時にチェックされません。
こういったことから分かるのは、通常のアプリケーション用プログラムをコーディングするうえでは、java.lang.Errorを使うことはないということです。ただし、java.lang.Errorによるエラーが発生した場合にはシステムの何かがおかしくなっているということだけは覚えておきましょう。
ここまでの説明から、「コンパイル時にチェックされない例外やエラー」を独自に実装するには、java.lang.Errorを継承する方法と、java.lang.RuntimeExceptionを継承する方法があるということに気が付くはずです。
しかし、JavaVMが実行不能になるような異常な状態を表現する例外やエラーを独自に実装することは基本的にはないので、もし「コンパイル時にチェックされない例外やエラー」を独自に実装したいといった場合は、java.lang.RuntimeExceptionを継承するのが普通です。
ここまで「例外をキャッチする」という表現をしてきましたが、「キャッチする」に対応するのは「スローする(投げる)」になります。例外が発生するメソッドでは、どんな例外が発生するかが分かるように、どんな例外を投げるかについて宣言がされています。
例えば、「java.io.InputStreamReader」のcloseメソッドのAPIリファレンスを見ると、次のように記述されています。
public void close() throws IOException ストリームを閉じます。 定義: インタフェース Closeable 内の close 定義: クラス Reader 内の close 例外: IOException - 入出力エラーが発生した場合
このことから、java.io.InputStreamReaderクラスのcloseメソッド内では、java.io.IOExceptionという例外が発生する可能性があることが分かります。
また、APIリファレンスでのメソッドの宣言を見ても分かるように、プログラムでは、この例外が発生することを宣言するために、「throws(投げる)」というキーワードを使います。
今回は、ここまでです。次回は、独自の例外を定義する方法について紹介します。
今回作ったサンプルのソースコードは、こちらからダウンロードできます。
小山博史(こやま ひろし)
情報家電、コンピュータと教育の研究に従事する傍ら、オープンソースソフトウェア、Java技術の普及のための活動を行っている。長野県の地域コミュニティである、SSS(G)やbugs(J)の活動へも参加している。
著書に「基礎Java」(インプレス)、共著に「Javaコレクションフレームワーク」(ソフトバンククリエイティブ)、そのほかに雑誌執筆多数。
Copyright © ITmedia, Inc. All Rights Reserved.