KATOエンジニヤリング開発日誌

「アウトプット無きエンジニアにインプットもチャンスも無い」の精神で書いています

Javaの例外処理について学ぶ

「Javaプログラミング技法」の第13回目の授業内容まとめになります。

※前回授業の内容はこちら

www.kato-eng.info

例外処理の基礎

プログラムの実行中に発生するエラーを例外といい、例外の発生時に正常時の実行フローから異常時の実行フローへ切り替えることを例外処理とよぶ。

例外が発生したときに放置すると、下記のような不具合が生じる可能性がある。

  1. 途中結果をファイルに書き込むはずが、ファイルのアクセス権が無く、書き込みに失敗した。
    • 本来ならこの段階で例外処理を行うべきだった
  2. プログラムは書き込みを失敗したまま、処理を先に進めてしまう。
  3. その後、プログラムが途中結果を確認すると、ファイルには何も書かれていなかった。
  4. プログラムは途中結果が空の文字列として処理が進み、予想できない動作を起こしてしまった。

エラーの種類

エラーには設計時のエラーと実行時のエラーの2つがある。

設計時のエラー

文法の間違いや変数に異なる型の値を代入したりするなどのコンパイル時にエラーが通知されるもの。

int x;

// int型の変数「x」に文字列を代入する
x = "String Value";

// for文の閉じ括弧を付け忘れる
for (int i = 0; i < 10; i++) {
    System.out.println(i)

設計時のエラーはそもそもコンパイルが通らないので、例外処理で対応する必要がない。

実行時のエラー

コンパイル時にはわからず、実行時にエラーが通知されるもの。

BufferedReader br = new BufferedReader(new Input StreamReader(System.in));

// キーボードからの入力をString型として読み取る
String str = br.readLine();
// 下記処理で数値に変換するが「ABC」等の数値型に変換できない文字を入力する

// 読み取った文字列をint型に変換
int n = Integer.parseInt(str);

他にも、ある数値を0で割ったりしたときにも実行時のエラーが発生する。実行時のエラーは例外処理で適切に処理を行う必要がある。

例外の対応

プログラムが途中で止まってしまうのは例外が発生したときの処理を書いていないから発生する。例外が発生したときの処理を適切に書いておけばプログラムを正しく終了させることができる。

例外の基本構文

  • tryブロックで例外が発生せず正常に処理が終了した場合はfainallyブロックに遷移する
  • tryブロックで例外が発生した場合には処理を中断してcatchブロックへ遷移する
  • fainallyブロックはtryブロックで例外の発生有無にかかわらず実行される
try {
    // 例外が発生する可能性のある正常時の処理
} catch (例外のクラス 変数名) {
    // 指定したクラスの例外が発生した場合に実行される処理
} finally {
    // 例外発生の有無にかかわらず実行される処理
}

finallyブロックは省略可能。finallyブロックはファイルの読み書きや通信の処理等に使用されることが多い。

例外処理の例

// ExceptionSample.java

public class ExceptionSample {
    public static void main(String[] args) {
        try{
            // try{}で例外が発生する可能性がある部分を囲む
            int[] a = new int[5];

            System.out.println("a[10]に値を代入します。");
            // 例外が発生する処理
            a[10] = 80;
            // 1つ上の処理で例外が発生するので次の標準出力は実行されない
            System.out.println("a[10]に値を代入しました。");
        } catch (ArrayIndexOutOfBoundsException e) {
            // catch{}内に例外が発生したときの処理を記述する
            System.out.println("配列の要素を越えています。");
        } finally {
            System.out.println("この処理は必ず実行されます。");
        }
        System.out.println("処理が終了しました。");
    }
}

出力結果は下記の通り。

a[10]に値を代入します。
配列の要素を越えています。
この処理は必ず実行されます。
処理が終了しました。

例外クラス

例外は例外のクラスから生成されたインスタンスであり、例外の種類に応じてクラスが異なる。例外クラスとそのインスタンスが提供するメソッドを使うことで例外の詳しい情報を取得することができる。

try {
    // 例外が発生する可能性のある処理
} catch (ArrayIndexOutOfBoundsException e) {
    // ArrayIndexOutOfBoundsException = 例外のクラス(Throwableクラスのサブクラスである)
    // e = 例外クラスが代入される変数(慣例的に「e」を使うことが多い)
}

tryブロック内の処理を実行中、catchで指定したクラスの例外が発生すると例外の種類に応じたcatchブロックに遷移する。この時に例外クラスのインスタンスが変数「e」に代入される。但しcatchできるのはThrowableクラスのサブクラスの例外のみである。

Throwableクラスとサブクラス

f:id:masayuki_kato:20170730130147p:plain

例外クラスのインスタンスを扱う

「System.out.println」の中で例外クラスのインスタンスを出力すると、Objectクラスから継承した「toString()」が呼ばれ、例外の種類(クラス名)がわかる。

// ExceptionSample2.java

public class ExceptionSample2 {
    public static void main(String[] args) {
        try {
            int[] a = new int[5];
            System.out.println("a[10]に値を代入します。");

            // ArrayIndexOutOfBoundsExceptionが発生する処理
            a[10] = 80;
            System.out.println("a[10]に80を代入しました。");
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("配列の要素を越えています。");
            // eに代入されている例外の情報をtoString()で取り出し、出力する。
            System.out.println(e + "という例外が発生しました。");
        }
        System.out.println("無事終了しました。");
    }
}

出力結果は下記の通り。

a[10]に値を代入します。
配列の要素を越えています。
java.lang.ArrayIndexOutOfBoundsException: 10という例外が発生しました。
無事終了しました。

複数種類の例外処理

catch文を複数並べて記述することで、複数種類の例外発生に対応することができる。例外の種類にスーパークラスの例外クラスを指定すると、そのサブクラスの例外クラス全てをcatchすることができる。「Exception」はcatch可能な全ての例外のスーパークラスなので、発生するすべての例外をcatchすることができる。例外を複数並べると、catch文の上から条件判定されるので、Exceptionクラスの下に記述した例外は発生してもExceptionクラスの例外でcatchされてしまうので注意が必要である。

try {
    // 例外が発生する可能性のある処理
} catch (ArrayIndexOutOfBoundsException e) {
    // 配列の要素が越えてしまう例外発生時の処理を記述
} catch (IOExcetpion e) {
    // 「ArrayIndexOutOfBoundsException」に該当せず、IO処理で例外が発生した際の処理を記述
} catch (Exception e) {
    // 「ArrayIndexOutOfBoundsException」「IOException」に該当しない全ての例外をcatchした時の処理を記述
}

例外の作成と送出

Javaに用意されている例外クラスとは別の例外を独自に宣言することができる。宣言した例外は任意のタイミングで送出することができる。この例外を送出するクラスを利用する人に例外処理を強制することができ、何か例外処理を書かないとコンパイルできないエラーに強いプログラムを作成することができる。

独自の例外クラスを作成するにはExceptionクラスを継承すればよい。例外に必要なメソッドは、すべてスーパークラスであるExceptionとThrowableクラスから引き継ぐことができる。Exceptionクラスを継承したクラスのインスタンスは「try-catch」でcatchすることができるようになる。

// 例外を送出する可能性があるメソッドの宣言方法
戻り値の型 メソッド名(引数リスト) throws 例外クラス

// 例外の送出方法
throw 例外オブジェクトを指す変数

/*
 * メソッド宣言時は throws
 * 例外送出の時は throw
 */

例外の送出例

独自例外のクラス

package jpt13;

public class StudentIDOutOfRangeException extends Exception {
    // Studentクラスのインスタンスに不正なIDを指定したときに発生する例外
}

例外の送出側

package jpt13;

public class Student {
    private int studentID;

    public Student() {
        this.studentID = 99999;
    }

    // setStudentIDメソッドがStudentIDOutOfRangeExceptionを送出する可能性があることを宣言する
    public void setStudentID(int id) throws StudentIDOutOfRangeException {
        if (id < 0) {
            // idが負数の場合例外のインスタンスを作成後、「throw」で送出する
            StudentIDOutOfRangeException e = new StudentIDOutOfRangeException();
            throw e;
        } else {
            studentID = id;
        }
    }

    public int getStudentID() {
        return studentID;
    }
}

例外を受け取る側

package jpt13;

public class ExceptionSample3 {
    public static void main(String[] args) {
        // Studentクラスのインスタンスを作成、初期idは99999。
        Student st = new Student();

        try {
            // idに「-10」を指定したためsetStudentIDメソッド内で例外が発生する
            st.setStudentID(-10);
        } catch (StudentIDOutOfRangeException e) {
            // 発生した例外(StudentIDOutOfRangeException)はここでcatchされる
            System.out.println(e + "をキャッチしました。");
        }
        /*
         *  例外処理をしているため、処理が継続されている。
         *  例外発生時は変数「id」に値を代入していないため、
         *  コンストラクタで代入した「99999」を取得している。
         */
        System.out.println(st.getStudentID());
    }
}

出力結果

jpt13.StudentIDOutOfRangeExceptionをキャッチしました。
99999