例外2

<概要>例外の階層と catch 節について学びます。どこで例外がキャッチされるのかをよく理解して下さい。

次の図は例外のクラス階層(継承関係)を示しています。

Object
 └- Throwable
   ├- Error
   └- Exception
     ├- RuntimeException
     └- それ以外のException

Throwable というクラスは全ての例外のスーパークラスです。
つまり、throw 文で投げることができ、catch 節で受け止めることができるクラスは、
全てこの Throwable クラスを継承しています。

Throwable という英単語は、throw (投げる)と -able (…できる)からできている語で、
「投げることができる」という意味になります。

Throwable のサブクラスは、大きく2つに分類できます。
Error と Exception です。

Error は、もはや動作を継続することを期待できないときに投げられるクラスです。
例えば、メモリが足りなくなった、スタックオーバーフローを起こした、などの場合です。

Exception は、正しく例外処理を行って、動作が継続することを期待するときに投げられるクラスです。
例えば、配列の範囲を超えてアクセスしようとした、
ファイルをオープンしようとしたが見つからなかった、などの場合です。

Exception は、更に RuntimeException とそれ以外に分けられます。

RuntimeException は、実行中に起こり、コンパイラによって前もってチェックされない例外を表すクラスです。
例えば、配列の範囲を超えてアクセスしようとした、整数で 0 の割り算を行った、
文字列を数に変換しようとしたが正しい形式ではなかった、などの場合です。

RuntimeException 以外の Exception は、コンパイラによって前もってチェックされる例外を表すクラスです。
例えば、ファイルをオープンしようとしたが見つからなかった、
ファイルを読み込もうとしたらファイルの終わりまでたどり着いた、などの場合です。

チェックされない例外= RuntimeException と Error
チェックされる例外= RuntimeException 以外の Exception


*=*=*=*=*=*=*=*=*=*= チェックされる例外 *=*=*=*=*=*=*=*=*=*=

「チェックされる例外」を投げるプログラムを書くならば、
プログラマは次の2つのいずれか1つを「必ず」行っておかなければなりません。

(1) catch 節でその例外をキャッチする

void someMethod(){
  try{
    ……
    throw new IOException();
    ……
  }catch(IOException e){
    ……
  }
}

(2) メソッドの throws 節でその例外を投げることを宣言する

void someMethod() throws IOException{
  ……
  throw new IOException();
  ……
}

「チェックされない例外」を投げる場合に行わなければならないことはありません。

*=*=*=*=*=*=*=*=*=*= 実行時の例外について *=*=*=*=*=*=*=*=*=*=

「ファイルが見つからない( FileNotFoundException )」という例外は、
RuntimeException のサブクラスではありません。
つまり、実行時の例外ではないのです(チェックされる例外)。
でも、オープンしようとしているファイルがあるかどうかは、実行時にしか分かりませんよね。
これはどう考えたらいいのでしょう?
もちろん、本当にファイルが見つからない事態が発生するかどうかは、実行時にしか分かりません。
でも、ファイルをオープンしようとしたときに
ファイルが見つからない可能性があるということは、コンパイル時に分かることです。
「 FileReader のコンストラクタが FileNotFoundException を投げる」というのは、
「 FileNotFoundException を投げる可能性がある」ということなのです。

例題13-2-1 以下に示すプログラムにおいて、どこで例外がキャッチされるでしょうか?

○プログラム

public class ExceptionQuiz{
  public static void main(String[] args){
    System.out.println("START");
    try{
      int[] a=new int[3];
      System.out.println("代入します");
      a[3]=123;
      System.out.println("代入しました");
    }catch(RuntimeException e){
      System.out.println("catch(1)");
    }catch(Exception e){
      System.out.println("catch(2)");
    }finally{
      System.out.println("finally");
    }
    System.out.println("END");
  }
}


○実行結果

D:\atsushi\Java\List13-10>java ExceptionQuiz
START
代入します
catch(1)
finally
END
-- Press any key to exit (Input "c" to continue) --

○解説

投げられた例外をキャッチするのは、一番早くマッチした catch 節です。
ただし、投げられた例外と全く同じクラスを使わなくても、
そのスーパークラスならば、例外をキャッチすることが出来ます。

したがって、クラス階層が重要になってくるんです!

このプログラムで投げられる例外は ArrayIndexOutOfBoundsException です。
ArrayIndexOutOfBoundsException のクラス階層は以下のようになっています。

java.lang.Object
 |
 +--java.lang.Throwable
  |
  +--java.lang.Exception
   |
   +--java.lang.RuntimeException
    |
    +--java.lang.IndexOutOfBoundsException
     |
     +--java.lang.ArrayIndexOutOfBoundsException


これより、プログラムでは一番早くマッチする RuntimeException で例外がキャッチされます。

ここで仮に、投げられた例外が IOException ならば、
RuntimeException ではマッチしないので、
次の catch 節を見て、Exception でマッチするので、ここでキャッチされます。

IOException のクラス階層は以下のようになっています。

java.lang.Object
 |
 +--java.lang.Throwable
  |
  +--java.lang.Exception
   |
   +--java.io.IOException


try … catch … catch …は、switch … case … case …に似ていますね。

finally ブロックは、try ブロックの中で例外が投げられても、投げられなくても必ず実行されます。
try ブロックの中で return する場合でも必ず実行されます。
したがって、finally ブロックの中には、try ブロックの中で行ったことの後始末を書いておきます。
一つ注意しなければならないのは、finally ブロックの中で return 文を書いてはいけない、ということです。
後始末を途中で止めてしまうわけにはいきませんよね。

ここで仮に、例外が発生しなかった場合はどうなるでしょうか?
a[2]=123; として実行してみましょう。

D:\atsushi\Java\List13-10>java ExceptionQuiz
START
代入します
代入しました
finally
END
-- Press any key to exit (Input "c" to continue) --

例外が発生しなくても finally ブロックが実行されていることが分かりますね。

○補足

キャッチされなかった例外は、コールスタックを次々にさかのぼって、
「私を受け止めてくれる catch 節」を探しに行きます。
最後まで見つからなかった場合には、プログラムが終了します。

○補足2

以下に示すように、サブクラスより先に
スーパークラスでキャッチするようなプログラムはコンパイルエラーです。
なぜなら、サブクラスでキャッチされることは絶対に無いからです。

try{
 ……
}catch(Exception){
 ……
}catch(IOException){
 ……
}


例題13-2-2 例外を発生したメソッドはどういう経路で呼び出されていたかを出力する。

○プログラム

public class ExceptionTest4{
  public static void main(String[] args){
    int[] myarray=new int[3];
    try{
      System.out.println("代入します");
      myAssign(myarray,100,0);
      System.out.println("代入しました");
    }catch(ArrayIndexOutOfBoundsException e){
      System.out.println("代入できませんでした");
      System.out.println("例外は"+e+"です");
      e.printStackTrace();
    }
    System.out.println("終了します");
  }
  static void myAssign(int[] arr,int index,int value){
    System.out.println("myAssignに来ました");
    yourAssign(arr,index,value);
    System.out.println("myAssignから帰ります");
  }
  static void yourAssign(int[] arr,int index,int value){
    System.out.println("yourAssignに来ました");
    arr[index]=value;
    System.out.println("yourAssignから帰ります");
  }
}


○実行結果

D:\atsushi\Java\List13-7>java ExceptionTest4
代入します
myAssignに来ました
yourAssignに来ました
代入できませんでした
例外はjava.lang.ArrayIndexOutOfBoundsException: 100です
java.lang.ArrayIndexOutOfBoundsException: 100
  at ExceptionTest4.yourAssign(ExceptionTest4.java:22)
  at ExceptionTest4.myAssign(ExceptionTest4.java:17)
  at ExceptionTest4.main(ExceptionTest4.java:6)
終了します
-- Press any key to exit (Input "c" to continue) --

○解説

public class Throwable extends Object implements Serializable  //java.lang パッケージ
 Throwable クラスは、Java 言語のすべてのエラーと例外のスーパークラスです。

public void printStackTrace()  //Throwable クラス ← java.lang パッケージ
 このスロー可能オブジェクトおよびそのバックトレースを標準エラーストリームに出力します。

それでは、printStackTrace メソッドが出力した内容を見ていきましょう。

at ExceptionTest4.yourAssign(ExceptionTest4.java:22)
 → 22 行目で例外が発生しました。

at ExceptionTest4.myAssign(ExceptionTest4.java:17)
 → 例外が発生したメソッドは 17 行目で呼び出されたものです。

at ExceptionTest4.main(ExceptionTest4.java:6)
 → 更にそのメソッドは 6 行目で呼び出されたものです。

例外は「私を受け止めてくれる catch 節」を探して、
コールスタックを次々にさかのぼります。
ここで仮に、yourAssign メソッドの中で例外をキャッチした場合は、
それ以降の処理はいつも通り実行され、
main メソッドの catch ブロックは実行されません。
しかし、yourAssign メソッドの中に書いた例外処理で例外をキャッチできなかった場合は、
やはり main メソッドの中の catch ブロックに飛ぶことになります。

○補足

例外クラスは自分で作っても構いません。
しかし、RuntimeException や Error のサブクラスとして宣言するのは出来るだけ避けましょう。
これらのクラスは「チェックされない例外」ですので、コンパイラはノータッチになるからです。
どのメソッドがどの例外を投げるのか、どこでキャッチするのか、コンパイラは何もチェックしてくれません。
これら全てをプログラマが管理するのは大変ですよね。

例題13-2-3 例外がどのメソッドから投げられたか出力する。

○プログラム

public class ExceptionTest5a{
  public static void main(String[] args){
    try{
      method1(0);
      method2(0);
      method3(0);
    }catch(Exception e){
      System.out.println("例外:"+e);
      e.printStackTrace();
    }
  }
  static void method1(int x) throws Exception{
    if(x>0){
      throw new Exception();
    }
  }
  static void method2(int x) throws Exception{
    if(x==0){
      throw new Exception();
    }
  }
  static void method3(int x) throws Exception{
    if(x>0){
      throw new Exception();
    }
  }
}


○実行結果

D:\atsushi\Java\ListA13-2a>java ExceptionTest5a
例外:java.lang.Exception
java.lang.Exception
  at ExceptionTest5a.method2(ExceptionTest5a.java:19)
  at ExceptionTest5a.main(ExceptionTest5a.java:5)
-- Press any key to exit (Input "c" to continue) --

○解説

printStackTrace メソッドが出力した内容を見ていきましょう。

at ExceptionTest5a.method2(ExceptionTest5a.java:19)

「19 行目 throw new Exception(); で例外が投げられた」ということは、
例外を投げたメソッドは method2 だというが分かります。

at ExceptionTest5a.main(ExceptionTest5a.java:5)

また、例外を投げたメソッドを呼び出したのは 5 行目 method2(0); ですから、
やはり例外を投げたメソッドは method2 だというが分かります。

例題13-2-4 メソッドの中で例外をキャッチする。

○プログラム

public class ExceptionTest3a{
  public static void main(String[] args){
    int[] myarray=new int[3];
    myAssign(myarray,100,0);
    System.out.println("終了します");
  }
  static void myAssign(int[] arr,int index,int value){
    System.out.println("myAssignに来ました");
    try{
      System.out.println("代入します");
      arr[index]=value;
      System.out.println("代入しました");
    }catch(ArrayIndexOutOfBoundsException e){
      System.out.println("代入できませんでした");
      System.out.println("例外は"+e+"です");
      e.printStackTrace();
    }
    System.out.println("myAssignから帰ります");
  }
}


○実行結果

D:\atsushi\Java\ListA13-1>java ExceptionTest3a
myAssignに来ました
代入します
代入できませんでした
例外はjava.lang.ArrayIndexOutOfBoundsException: 100です
java.lang.ArrayIndexOutOfBoundsException: 100
  at ExceptionTest3a.myAssign(ExceptionTest3a.java:11)
  at ExceptionTest3a.main(ExceptionTest3a.java:4)
myAssignから帰ります
終了します
-- Press any key to exit (Input "c" to continue) --

○解説

プログラムと実行結果より printStackTrace メソッドが出力するのは例外をキャッチした場所ではなく、
例外を発生したメソッドのコールスタックであることが分かります。

メソッドの中で例外をキャッチした後は、
メソッド呼び出し元に戻り、いつも通り処理をこなしていきます。
ここで仮に、myAssign の中だけでなく main メソッドの中にも例外処理があったとしても、
例外は既にキャッチされているので、man メソッドの例外処理は実行されず、
main メソッドの try ブロックを最後まで実行するだけです。

○補足

メソッドの中で何かエラーが起きたことを示すには以下の方法があります。

(1) 戻り値でエラーを示す
(2) 例外でエラーを示す
(3) 別の方法でエラーを示す

(3)の代表的な例が java.io.PrintWriter というクラスです。
このクラスで起こったエラーは戻り値や例外ではなく、checkError というメソッドを呼び出して調べます。
PrintWriter クラスはデバッグ出力で使われ、いちいちエラー処理をしないのが普通だからです。

コールスタック

メソッド呼び出しの積み重ねのことをコールスタックと言います。
コールスタック( call stack )とは、「呼び出し( call )」が「積み重なったもの( stack )」という意味です。


戻る / ホーム