Deep Side of Java〜Java 言語再入門 第2回 〜 Java 文法を中心に

例外






例外

さて、今まで後回しにしてきた「例外」である。C言語で言えば、setjmp(3),longjmp(3)を使って実装する非ローカル分岐である(setjmp,longjmp が判らない人は、次の解説を見よ!)が、これを形式化して catch & throw 機構に整理したものが「例外」である。C++ でも、例外は言語機能として実装されているが、それは C++ の機能の1つに過ぎない。しかし、Java では「例外」は言語の本質に関わる重大な機能を担当している。だから、Java の理解は「例外」の理解なくしてはおぼつかない。

例外の使い方

「例外」はどういう状況で使われるものであるかをまず説明しよう。これは「例外的な状況」に対する「エラー処理」である。例外を使わないエラー処理と言うと、NULL や -1 に代表される「帯域外の値」を戻り値として返すというのが一般的である。だから、関数の呼び出し結果をチェックして、このような「帯域外」の値であれば、エラー表示をして処理をせずにリターンするのが常道である。

しかし、Java ではこのような「帯域外の値」がチェックされずに無視される可能性を減らす目的で、例外が導入された。だから、例外はメソッドの戻り値の型と同じように、メソッドのプロトタイプの一部を形成し、あるメソッドが例外を「投げる」としたら、その関数の呼び出し元では、何らかの処理をしなければならないことを強制している。

例外は次のように使う。

try {
        array[n] = n;
        .............
} catch( IndexOutOfBoundsException e ) {
        System.out.println( "配列 array にサイズ外の参照 " + n 
                            + " がなされました" );
        return;
}

この場合、もし n が配列の範囲外の値であれば、配列参照でエラーを検知すると、自動的に制御が catch 以下の部分(catch 節)に移る。そして、エラー処理を行う。だから、配列参照以下の部分はまったく実行されない。IndexOutOfBoundsException もやはり Throwable クラスから派生したクラスであり、例外を投げる throw 文で、通常インスタンスを作成して返す。だから、このような例外クラスにはエラー内容をセットできる。次の例は明示的な配列チェックによって「例外を投げる」。

try {
        if( n >= array.length || n < 0 ) {
                throw new IndexOutOfBoundsException( "配列添字が異常です" );
        }
        array[n] = n;
        .............
} catch( IndexOutOfBoundsException e ) {
        System.out.println( "配列 array にサイズ外の参照 " + n 
                                           + " がなされました" );
        System.out.println( e.getMessage() );
        return;
}

例外クラスは toString メソッドを "例外クラス名: コンストラクタに与えた文字列" の文字列化をするように Exception クラスで上書きしている。だから、System.out.println() のような String 型を要求するメソッドでは、適切なメッセージとして文字列化されることになる。だから System.out.println( e ); の方がより適切である。

例外と型宣言

この例外機構は、Java ではメソッドの型と合体している。つまり、あるメソッドがどういう例外を投げるのかを、もし throw 文を使うのであれば宣言しなければならない。

public int expThrower( int [] array, int n ) throws Exception {
        if( n >= array.length || n < 0 ) {
                throw new Exception( "配列添字が異常です" );
        }
        array[n] = n;
        .............
}

............
        try {
                int v = expThrower( array, 3 );
                .......................
        } catch( Exception e ) {
                System.out.println( e );
        }

もし、呼び出し元で例外を catch することを忘れていると、「例外 java.lang.Exception は報告されません。スローするにはキャッチまたは、スロー宣言をしなければなりません。」というようなエラーメッセージが出て、コンパイルは失敗する。つまり、サブルーチンで発生するかも知れない異常な事態に対する対応を、呼び元に強制することができるのである。ここで、IndexOutOfBoundsException から Exception に投げるクラスを変更したのは次の事情による。

配列アクセスなどの実行時例外は、もし型チェックの対象にして必ず呼び元に処理をさせようとすると、繁雑になるばかりであるし、また、実行時例外によってはその例外が発生するかどうかチェックのしづらいものも多い。だから、一部の例外は型チェックの対象としないことになっている。これを、例外クラスの階層構造によって実現している。

Throwable --- Error     --- 各種の JVM 自体のエラーを通知するクラス
           |- Exception --- 非チェック例外(型チェックに含む例外)
                         |- RuntimeException --- 実行時例外(型チェックに含まない)

つまり、IndexOutOfBoundsException の親クラスは RuntimeException であり、これは型チェックに含まれない実行時例外であるわけである。しかし、Exception は「標準チェック例外」であるので、try〜catch 構造による呼び出し元の処理を強制するのである。だから、通常新しい例外を定義するのには、Exception クラスから派生させるのが普通である。

public class myException extends Exception {
        public myException( String mes ) {
                super( mes );
        }
}
.................

        void expThrower( ) throws myException {
                ..........
                throw new myException( "エラーメッセージ" );
        }
                .................

                try {
                        expThrower();
                } catch( myException e ) {
                        System.out.println( e );
                        e.printStacktrace();
                        System.exit( 1 );
                }

というような使い方になる。新しい Exception クラスのメソッド printStacktrace は、どのようなメソッド呼び出し順で呼ばれたメソッドで例外が投げられたかを追跡して表示するメソッドであり、デバッグの役にたつメソッドである。

とはいえ、明示的に総称的例外クラスである java.lang.Exception を投げたりキャッチしたりするのは良くない。なぜなら、想定外の例外が起きたときに、やはりそれもそのエラーハンドラによってキャッチされてしまい、その時に正しいエラー処理をして、回復ができるという保証がないからである。

例外クラス

ということは、例外クラスはかなり沢山あり、それぞれに違う状況で発生し、それを区別して取り扱うことができる。代表的な実行時例外には次のクラスが定義されている。

ArithmeticException
division by 0 である。
ArrayStoreException
配列の型に対して、代入不可能な型のデータが代入された
ClassCastException
キャスト型が不正(代入不能な型にキャスト)
NumberFormatException
よく Integer.parseInt( String ); などで発生するのをキャッチする。文字列を数値に変換しようとしたが、文字列を数字文字として解釈できない。
IndexOutOfBoundsException
配列添字が配列サイズを越えたり負であったりする。
NullPointerException
null ポインタによる参照が行われた。これはつまり、大概の場合に未初期化インスタンス変数(初期化されていないと内容は null)にアクセスした場合である(それと、null を返す関数戻り値の null チェックをしていない場合...)。

これらの実行時例外は、catch することは出来るが、メソッドの型チェックの対象とはならないことは既に述べた通り。型チェックの対象となる「標準チェック例外」の代表的なものは次の通り。

ClassNotFoundException
IllegalAccessException
InstantiationException
これら3つは、メタクラスオブジェクトを使って動的にインスタンスを生成するときに発生する。
InterruptedException
マルチスレッドプログラムの時に、明示的にスレッドに対して割り込みをかける時に使う例外。これは java.lang.Thread クラスの interrupt メソッドによって生成される。
java.io.IOException
これは java.io.* パッケージで定義される、入出力処理の失敗を一般に示す例外である。
java.io.FileNotFoundException
IOException のサブクラスで、ファイルが見つからない場合。
java.io.EOFException
IOException のサブクラスで、EOFを検出した。

などなど、さまざまな例外がそれぞれのパッケージで定義されている。また、一般にプログラマが自分で定義する例外は、この「チェック例外」のクラスに属し、処理を強制すべきである。

ということは、複数の例外処理を1つの try 〜 catch ブロックでする必要があるわけである。Exception 例外を catch して、その中で instanceof 演算子を使って判定することもできるが、複数の catch 節を書いて処理をした方が良い。

try {
        Class c = Class.forName( "java.lang.Vector" );
        Vector v = (Vector)c.newInstance();
        ...............
} catch( ClassNotFoundException e ) {
        System.out.println( "CLASSPATH の中に、クラス " +
                            "java.lang.Vector が見つかりません" );
} catch( IllegalAccessException e ) {
        System.out.println( "クラス java.lang.Vector をロードできません。" +
                            "public なクラスでないと考えられます" );
} catch( InstantiationException e ) {
        System.out.println( "クラス java.lang.Vector からインスタンスを" +
          "作れません。インターフェイスか抽象クラスである可能性があります。" );
}

また、try 〜 catch ブロックは、入れ子にすることもできる。だから、例外処理は try 〜 catch ブロックの入れ子を辿って、自分を処理する catch 節を探して遡る。これはメソッド呼び出しの階層を遡る。要するに、複数の try 〜 catch ブロックが入れ子になっている場合、投げられた例外をキャッチしない try 〜 catch ブロックは単に無視されて、一番「近い」その例外をキャッチする catch ブロックに制御が移る。これを「例外の伝播」と呼ぶ。

一般にエラーハンドラを書くわけでもない実行時例外の場合には、最終的にトップレベルのエラーハンドラに catch され、インタプリタが終了する。これには頻繁にお目にかかろう。

また、try 節と catch 節の双方の処理で後処理をするために、finally 節を加えることもできる。finally 節は、try 節と catch 節の処理が終った後に必ず実行される。

例外は大変有用なので、充分活用されるように願いたい。



copyright by K.Sugiura, 1996-2006