第18回 例外処理



第8回で挙げたMainArgsTestを思い出してみてください。
class MainArgsTest{
    public static void main(String [] args){
        System.out.println("args.length = " + args.length);
        System.out.println("args[0] = \"" + args[0] + "\"");
        System.out.println("args[1] = \"" + args[1] + "\"");
        System.out.println("args[2] = \"" + args[2] + "\"");
    }
}
これを次のコマンドラインで実行してみましょう。
java MainArgsTest 01 02
実行結果は次のようになります。
args.length = 2
args[0] = "01"
args[1] = "02"
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
        at MainArgsTest.main(MainArgsTest.java:6)
下線部の記述が見慣れませんね。これが「例外」(Exception)が発生したことを示すメッセージなのです。
今回はこの例外が発生する条件とその種類、処理方法について説明します。

■例外とは何か
コンパイル時にエラーは発生しないのですが、実行中言語的に意味をなさなくなってしまう場合、自動的に生成されるオブジェクトが例外です。
例外が生成されることをスローされる(throwされる/投げられる)と言いますが、スローされた例外がプログラム中のどこでもキャッチされる(catchされる/受け止められる)ことがない場合はプログラムは終了します。上記の例の下線部分は例外によるプログラム終了時、例外の種類と発生箇所を報告しているものです。

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 2
        at MainArgsTest.main(MainArgsTest.java:6)
このメッセージの意味は次のように解釈できます。
MainArgsTestクラスのmain()メソッド実行時に7行目で例外ArrayIndexOutOfBoundsExceptionが発生しました。
例外が発生した配列の要素数は2です。

■主な例外の発生条件
コンパイル時では分からないプログラムの不具合に遭遇したり、或いはユーザの操作がプログラムの想定外である場合例外が発生します。
パッケージ例外発生条件
java.langArithmeticException整数型同士の除算(演算子/または%)時に
0による除算を行おうとした
java.langArrayIndexOutOfBounds
Exception
正しいインデックス(0〜配列要素数-1)
ではない値で配列要素にアクセスしようとした
java.langAssertionErrorアサーションを行う設定で、
アサーション条件式がfalseを返した
java.ioIOException入出力に関係するメソッドの
実行に失敗した
java.langNoClassDefFoundError指定されたクラス定義が見つからない
java.langNullPointerException整数型同士の除算(演算子/または%)時に
0による除算を行おうとした
java.langRuntimeException実行時に何らかの異常が発生した
java.langStackOverflowError再起回数が多い為、使用していた
スタック領域が溢れた
上記のMainArgsTestの場合、コマンドライン引数が3個(以上)あるという想定で作られています。その為コマンドラインの引数が2個(以下)になると、例外が発生するのです。
もしもMainArgsTestをこのように修正すれば、上記の例外は発生しなくなります。
class MainArgsTest2{
    public static void main(String [] args){
        int i;
        System.out.println("args.length = " + args.length);
        for (i=0;  i < args.length; i++){
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}
こうすると配列argsの配列要素にアクセスするのは常にargs[0]〜args[配列要素数-1]となります。
配列要素数が0であれば配列要素にアクセスすることはなくなりますので、実行コマンドがjava MainArgsTest2であっても例外は発生しません。
配列要素を指すインデックスの値が有効であるか否かはコンパイル時にはチェックせず、実行時に確認して異常であれば例外とします。
その為このように固定長配列の範囲外の配列要素を指した場合でもプログラムはコンパイルエラーにはならず、実行時に例外ArrayIndexOutOfBoundsExceptionが発生します。
public class ExceptionTest {
    public static void main (String [] args) {
        int intArray[] = new int[3];
        intArray[3] = 10;
    }
}

■例外の種類
これまで説明してきた例外とは、実は広義の例外であるThrowable(java.lang.Throwable)に関する説明でした。Throwableはthrow+able、「投げられる」「throwできる」という意味があります。
Throwableは次のように大別されます。
例外の種類

○Error
通常のアプリケーションではキャッチすべきではない重大なエラーです。上記の表で名前が「〜Error」となっているものすべてがErrorです。
Errorをスローする簡単なプログラムを挙げます。
public class StackOverFlowErrorTest{
    public static void main(String [] args){
        main(args);
    }
}
こうするとmain()メソッドからmain()メソッドを呼び、更にそのmain()メソッドからmain()メソッドを……と再起的な関数呼び出しが際限なく行われる為、StackOverflowErrorが発生します。
このプログラムはそのままでは止まらないので、コマンドプロンプト上でCtrlキーとZキーを同時押ししてください。
もっと手軽にErrorメッセージを確認したいのであれば、コマンドプロンプトから「java 存在しないクラス名」と入力しても構いません。こうすれば次のようなメッセージが出力されるはずです。
Exception in thread "main" java.lang.NoClassDefFoundError: 実在しないクラス名

○検査例外
検査例外の検査とは、Javaコンパイラによる検査を意味しています。
検査例外をスローするコードを記述したり、使われているメソッドが検査例外をスローする可能性があることを明示している場合は、後述する例外処理法でその例外に対応するようにしていないとコンパイルエラーとなります。
実行時例外と対を成す存在であり、実行時例外ではないExceptionは全て検査例外となります。上記の表ではjava.io.IOExceptionのみが検査例外です。

○実行時例外
実行時例外は別名非検査例外とも呼ばれます。その名の通り後述する例外処理の対象にする必要がない(しなくてもコンパイルエラーにはならない)例外のことです。
java.lang.RuntimeExceptionとそのサブクラスが実行時例外です。上記の表ではjava.io.IOException以外の全ての〜Exceptionが実行時例外です。

■独自例外の作成方法
例外は既存のものを使うばかりではなく、独自の例外を自分で定義することもできます。その場合、例外もクラスですから、既存の例外のサブクラスとして定義するのです。
たとえば実行時例外としてDokujiExceptionを定義したい場合は次のように記述します。
class DokujiException extends java.lang.RuntimeException{
    独自例外固有の処理
}

■例外の生成方法
通常、例外は問題発生時にJVMによって自動的に生成されます。ですが、全てJVMに任せなければならないわけではなく、プログラマが明示的に例外を生成してスローすることも可能です。
明示的な例外の生成とスローにはthrowを用います。
throw 例外オブジェクト
例外オブジェクトは上記の例外を型名としたオブジェクトの識別名です。たとえばRuntimeExceptionをスローしたい場合は次のようにします。
RuntimeException re = new RuntimeException();
throw re;
これを1行にまとめてこのように記述することも可能です。
throw new RuntimeException();
既存・独自いずれの例外も、このやり方で生成することが可能です。
次のプログラムを実行して、その実行結果を確認してみてください。
class ThrowRuntimeException {
    public static void main(String [] args){
        throw new RuntimeException();
    }
}

■例外の処理方法(throws)
発生した例外は先に少し触れたようにキャッチする(catchする/捕捉する、受け止める)ことで使用することができます。それには2種類の方法がありますが、まずはthrowsを用いた方法について説明します。
throwsはコンストラクタ/メソッド宣言時のキーワードであり、次のように記述します。
[修飾子] 戻り値型 メソッド名 ([パラメータリスト]) throws 例外名 {
    メソッドの実装部分
}
[]内は省略可能です。また、コンストラクタの場合戻り値型は付けません。
こうすることにより、そのコンストラクタやメソッドの実装部分で検査例外が発生した場合でもコンパイルエラーを起こさなくなります。
但し検査例外をthrowsで指定した場合、このメソッドやコンストラクタを呼び出す側では後述するcatchブロックでその検査例外に対する対応を記述しなければなりません。対応が記述されていない例外がある場合、コンパイルエラーになります。
throwsの例外指定は複数記述可能です。その場合カンマ区切りで
例外1, 例外2, 例外3...
のように記述していきます。

■例外の処理方法(try〜catch〜finally)
例外を処理するもう一つの方法はこれです。こちらは第9回で学習したswitch文と似ており、メソッド内に記述します。例外発生時には、その例外の種類によって処理を分岐させるのです。
try{
    例外が起きる可能性のある処理
}
catch (例外1 識別子1) {
    例外1発生時処理
}
catch (例外2 識別子2) {
    例外2発生時処理
}
......
finally{
    共通処理
}
try直後の中括弧{}で囲まれたtryブロック内で例外が発生すると、catchの直後にある例外名を参照します。
当てはまる例外名があればそのcatchの直後にある中括弧{}内のcatchブロックを実行します。switch文と異なり、複数のcatchブロックを実行することはありません。
当てはまる例外名がないか、例外が発生しなかった場合はいずれのcatchブロックも実行しません。
発生したcatchブロック内では小括弧()内の識別子を参照変数のようにして、生成された例外オブジェクトを扱うことができます。たとえばこのプログラムの場合、例外発生時にその例外の名前を表示します。
public class ExceptionObject {
    public static void main (String [] args) {
        try{
            System.out.println("*** START ***");
            int intArray[] = new int[3];
            intArray[3] = 10;
        }
        catch(ArrayIndexOutOfBoundsException e){
            System.out.println(e);
        }
        finally {
            System.out.println("*** END ***");
        }
    }
}
catchブロックを実行した場合もそうでない場合も、必ずfinallyブロックを実行します。たとえcatchブロック内にreturn文を書いていたとしても、finallyブロックは実行されます。finallyブロックを実行させずに処理を打ち切らせたい場合はSystem.exit()メソッドを実行するしかありません。
catchブロックの例外名はサブクラス→スーパークラスの順に並ぶようにする必要があります。この逆にするとコンパイルエラーになります。
try{
    例外が発生する可能性のある処理
}
catch (Exception e){
    System.out.println("Exception");
}
catch (RuntimeException e){
    System.out.println("RuntimeException");
}
このような記述はコンパイルエラーになります。正しくコンパイル・実行できるようにするには次のように記述しなければなりません。
try{
    例外が発生する可能性のある処理
}
catch (RuntimeException e){
    System.out.println("RuntimeException");
}
catch (Exception e){
    System.out.println("Exception");
}

また、tryブロック内で発生する可能性のない検査例外に対する例外に対してcatchブロックを記述してもコンパイルエラーになります。
class IllegalCatchBlock{
    public static void main(String args []){
        try{
            System.out.println("args[0] = " + args[0]);
        }
        catch (java.io.IOException e){
            System.out.println("IOException");
        }
        finally{
            System.out.println("END");
        }
    }
}
この場合次のように修正すればコンパイル・実行共に可能になります。
class LegalCatchBlock{
    public static void main(String args []){
        try{
            System.out.println("args[0] = " + args[0]);
        }
        catch (RuntimeException e){
            System.out.println("RuntimeException");
        }
        catch (Exception e){
            System.out.println("Exception");
        }
        finally{
            System.out.println("END");
        }
    }
}
catchブロックはtryブロックの内容を確認し、発生する可能性があり且つ対応する必要の例外についてのみ記述するようにしてください。

次回はファイル等からの入出力を行わせる方法について説明します。
今回説明した検査例外をスローするメソッドもありますので、例外処理は不可欠です。例外処理方法を体得してください。

質問はこちらへお願いします(件名はそのままで)。