メソッドからはreturn文で戻り値が返されます。実はもうひとつ例外というものが戻ってくることがあるのです。例外の場合は、戻されるというより、投げつけられるという感じですか。身をかわすもよし、しっかり受け止めるのもよし、お好みでどうぞ。
お米ないよぉ
プログラムの実行中にエラーが起こった場合、Javaでは例外と呼ばれるオブジェクトが発生します。発生する可能性のある例外に対する処理を、あらかじめ記述しておくことで、プログラムがそのエラーにより終了してしまうことを避けることができます。
メソッド内で例外が発生した場合、呼び出し元へそのまま例外を渡してしまうことができます。これを、「例外が投げられる」といいます。呼び出し元では例外を受け取りますが、そのまた呼び出し元にポイッとそのまま投げてしまうことも可能です。
また例外に対する処理をtry~catchであらかじめ指定しておくこともできます。これをハンドラといいます。例外は、mainに戻るまでのどこかの時点でハンドラにより処理される(これを「例外が捕捉される」といいます)必要があります。ただしtry~catchで処理すべき例外とそうでない例外があります。それは後ほど。
夕食後の皿洗いの後、息子に米とぎを頼むことがあります。終わるまでボーッとテレビを見て待っています。時々「米が足りないよぉ」とか「炊飯器おかしいよぉ」とかで、米とぎ続行不可能である旨、知らせてくることがあります。そんなときは重い腰を上げ、それぞれのトラブルに対処するのです。
我が家の米びつはボタンを押せば必要量だけお米が出てきます。妹がやりたがるので、兄ちゃんはお米をはかる作業だけ妹にやらせることにしました。妹はボタンを押してもお米が出てこないことがわかると、兄に「お米ないよぉ」と知らせます。兄はお米がなくては自分の作業も進めることができないので、母親にそのまま「お米ないよぉ」と言います。その様子を図にします。
妹はお米がないことを「お米ないよぉ」(=例外)と兄ちゃんに知らせて(投げて)います。兄ちゃんはそれを聞いてそのまま母親へ「お米ないよぉ」(=例外)と伝えています。(投げています。)母はそれを聞いて精米をするなど対応を(try~catchで)とるわけです。(我が家は自家製のお米なので、なくなるとコイン精米機で精米します。私がやるわけではないですけどね。)
米とぎどころじゃない!
例外の実体はオブジェクトです。例外を生成するクラス、例外クラスはjava.lang.Throwableのサブクラスで定義します。標準ライブラリにもたくさんの例外が定義されています。またオリジナルの例外を定義することもできます。
米とぎの場合、お米がなければ精米すればいいし、炊飯器が故障していればガス釜を使えばOKです。しかし、米とぎ作業を中断せざる終えないような事態は、他にも考えられます。例えば大地震が来た、など、米とぎどころではない事態はいろいろ考えられます。でもそんなことまで想定していたのではプログラムも何が何やらわからなくなってしまい、本筋の米とぎがどこかへ行ってしまいます。(本当はいつでも大地震が来たときのことを想定して備えておかねばならないのですがね。)ここで言いたかったことは、トラブルもいろいろ、ということです。
標準ライブラリの例外クラスはもちろんすべてThrowableのサブクラスですが、さらに図のように三つのグループに分けられます。プログラム中でチェックしなければならないのはBグループ(検査例外クラスといいます)で生成された例外です。米とぎでは、米がなかったり、炊飯器が故障したりがこれにあたります。それ以外は非検査例外クラスです。大地震の場合です。
検査例外はmainが終わるまでにtry~catchできちんと対応しておかないと、コンパイラに怒られます。非検査例外はプログラムでは放っておいてかまいません。またAグループの例外は放っておかねばなりません。
次に各グループにどんな例外クラスがあるかいくつか例をあげておきます。
Aグループ ThrowableのサブクラスErrorのサブクラス。まれにしか起こらず対処することは不可能、あるいは対処すべきでないもの。次のようなクラスがある。
java.lang.OutOfMemoryError メモリ不足
java.lang.StackOverflowError スタックオーバーフロー
Bグループ ThrowableのサブクラスExceptionのサブクラスでCグループ以外のもの。mainまでのどこかでtry~catchにより対処しなければならないもの。
java.io.IOException 入出力エラー
java.io.FileNotFoundException ファイルが見つからない(IOExceptionのサブクラス)
java.lang.ClassNotFoundException クラスが見つからない
Cグループ ThrowableのサブクラスExceptionのサブクラスRuntimeExceptionのサブクラス。プログラムの実行中どこでも起こりうるもので、プログラム作成者のミスによるもの。
java.lang.ArithmeticException 整数演算での0による除算
java.lang.ArrayIndexOutOfBoundsException 配列の添字の不正
java.lang.IllegalArgumentException 引数の値のエラー
java.lang.NullPointerException nullポインタへのアクセス
投げるかもよ
呼び出し元に投げる可能性のある検査例外は、そのクラスをthrowsで指定する必要があります。指定しないとコンパイルエラーになります。E1、E2、E3は例外クラスとします。
public void methodA() throws E1, E2, E3 { : }
throwを使うと、クラスを直接指定して例外を投げることができます。
throw new E1();
newでE1のコンストラクタを直接呼び出しています。
throwとthrowsの使いどころを間違えないでください。
しっかりcatchしてね
try~catchを使うと、呼び出し元へそのまま例外を投げてしまうのでなく、そのハンドラの中で例外に対処することができます。
ではtry~catchの働きを見ていきましょう。次の図を見てください。最後にfinallyのブロックがありますが、これは例外が発生してもしなくても、プログラム終了前に必ず実行したい処理を記述します。いったん開いてしまったファイルを、例外が発生してもしなくても必ずクローズしたい、という場合などに使います。
①tryのブロックAで例外が発生しない場合、finallyのブロックDを実行後、そのまま次の処理に移ります。
②AでE1の例外が発生した場合、ただちにE1のcatch、Bに移り、終了後Dを実行します。
③AでE2の例外が発生した場合、ただちにCに移り、終了後Dを実行します。
④AでE3の例外が発生した場合、catchにE3の指定をしたものがないので、Dを実行後、処理を中断し、呼び出し元にE3の例外を投げます。
このハンドラを含むメソッドの1行目のthrowsには、どの例外クラスを指定すればいいのでしょうか。例外E1、E2については、このtry~catchで処理されたので、もう問題ありません。E3についてはfinallyのブロック終了後呼び出し元に投げられるので、E3をthrowsに指定しなければなりません。
精米機故障
ではcatchやfinallyのブロックを実行中に例外が発生したらどうなるのでしょうか。最初の例で、米がなくて精米しようとしたら、精米機が壊れていた(よくあるのです)、という場合ですね。
⑤E1の例外が発生し、さらにBの中でE4が発生した場合、D終了後メソッドを中断し、呼び出し元にE4の例外を投げます。
⑥DでE4が発生した場合、そこでただちにメソッドが中断し、呼び出し元にE4の例外を投げます。
投げるべきか、キャッチすべきか
ファイルからデータを入力しようとしたら、そんな名前のファイルはなかった、というケースを考えます。まだファイル入出力はやっていませんが、標準ライブラリのjava.ioパッケージのクラスを使って行います。そしてこの中のメソッドは、やはり標準ライブラリ中の例外を投げます。
入力ストリームをクラスjava.io.FileInputStreamのオブジェクトとして生成します。この時、指定した名前のファイルが存在しないとき、例外java.io.FileNotFoundExceptionが投げられます。次のようにメソッドの先頭行で「throws」の後に例外クラスの名前を指定しなければコンパイルエラーになります。ここではimportがあるのでパッケージ名は不要です。
[A.java]
import java.io.*; public class A { public void methodA() throws FileNotFoundException { //throwsがないと 「処理されない例外の型FileNotFoundException」 FileInputStream fi = new FileInputStream("data.txt"); } }
このメソッドを呼び出す場合、FileNotFoundExceptionが投げられる可能性があるので、さらにそれを呼び出し元に投げるか、次のようにtry~catchで処理します。
[Main.java]
import java.io.*; public class Main { public static void main(String[] args) { A a = new A(); try { a.methodA(); } catch(FileNotFoundException e) { e.printStackTrace(); } } }
catchの( )内のeは捕捉した例外が代入され、これは直後の{ }内で使用できます。ここではスタックトレースを出力しています。
次のようにmethodA内のtry~catchでFileNotFoundExceptionの例外を処理した場合、throwsにはこの例外の指定は不要です。また呼び出し元ではこの例外を投げられる心配はなくなります。
[A.java]
import java.io.*; public class A { public void methodA() { try { FileInputStream fi = new FileInputStream("data.txt"); } catch(FileNotFoundException e) { e.printStackTrace(); } } }
[Main.java]
public class Main { public static void main(String[] args) { A a = new A(); a.methodA(); } }
親子でなくて子親
ではせっかくだからファイルの先頭から100バイト分読み込んでみましょう。
import java.io.*; public class A { public void methodA() { int size; byte[] data = new byte[100]; try { FileInputStream fi = new FileInputStream("data.txt"); size = fi.read(data, 0, 100); : } catch(FileNotFoundException e) { //ファイルがなかった場合の処理 } catch(IOException e) { //入力エラーの処理 } } }
入力のメソッドreadはIOExceptionを投げる可能性があります。そこでcatchを追加してあります。さてこのcatchの順序を逆にするとどうなるでしょうか。
import java.io.*; public class A { public void methodA() { int size; byte[] data = new byte[100]; try { FileInputStream fi = new FileInputStream("data.txt"); size = fi.read(data, 0, 100); : } catch(IOException e) { System.out.println("IOError"); } catch(FileNotFoundException e) { //到達不可能なcatchブロック System.out.println("FileNotFound"); } } }
実はFileNotFoundExceptionはIOExceptionのサブクラスなのです。tryのブロックで例外が発生すると、catchを上から順に調べていって、該当する例外クラスの処理をするわけです。このときの“該当する”は、指定されたクラスが、例外のクラス自体か、そのスーパークラスの場合になります。つまりファイルがなかった場合、最初のcatchでひっかかってしまいます。そこで2番目のcatchのブロックは決して実行されないと、コンパイラにエラーにされます。catchに指定する例外クラスに親子関係がある場合には気をつけましょう。子→親の順で指定してください。
では面倒なので、スーパークラスのIOExceptionですべてcatchしてしまおう、と次のようにしたらどうでしょう。
import java.io.*; public class A { public void methodA() { int size; byte[] data = new byte[100]; try { FileInputStream fi = new FileInputStream("data.txt"); size = fi.read(data, 0, 100); : } catch(IOException e) { : } } }
もちろんこれでも、FileNotFoundExceptionの例外もキャッチできるのですが、ファイルがない場合と、入力エラーの場合では、対応もちがってくるはずです。やはりcatchできちんと処理を分けた方がきれいなプログラムになりそうですね。
親の愛
標準ライブラリの例外でないものが必要になった時、オリジナルの例外を定義することもできます。そのときはExceptionのサブクラスとして定義してください
public class Komenasi extends Exception { public Komenasi() { super(); } public Komenasi(String message) { super(message); } }
もしお米がなければ次のようにこの例外を投げます。
throw new Komenasi( );
throwsでこれこれの例外を投げるかもしれないよ、と言っているメソッドを上書き(オーバーライド)する場合、そのメソッドではこれこれ以外の例外をthrowsに指定することはできません。つまりスーパークラスのメソッドでthrowsに指定されている例外以外、サブクラスのメソッドでthrowsの指定に追加してはいけません。言い換えてもわかりにくいので例をあげます。
import java.io.*; public class ClassA { public void methodA() throws IOException { ; } }
ClassAのmethodAは例外IOExceptionを投げるかもしれないよ、となっていますね。
import java.io.*; public class ClassB extends ClassA { public void methodA() throws IOException { ; } }
ClassAのサブクラスClassBで、methodAを上書きしています。このthrowsにはIOExceptionが指定してあり、上と同じなのでOKです。もしここが
throws IOException, ClassNotFoundException
などとなっているとエラーになります。
import java.io.*; public class ClassC extends ClassA { public void methodA() throws FileNotFoundException { ; } }
ではこれはどうでしょう。FileNotFoundExceptionは、IOExceptionのサブクラスなので、OKです。ついでにこれは?
import java.io.*; public class ClassD extends ClassA { public void methodA() { ; } }
throwsがありませんが、上の規則で考えるとこれもOKです。
つまりスーパークラスにthrowsがないために、サブクラスで例外を投げられない、ということが起こり得るのです。例外を投げないクラスでも、将来生まれるであろうサブクラスのために、そこで投げる可能性のある例外は、throwsで指定しておくべきなのです。親が子を思いやる気持ち、というわけでしょうか。