パソナについて
記事検索

Javaのtry-catchとは?例外処理の方法をサンプルコードで解説

今回は、Javaの例外処理について具体例を交えながら説明していきます。例外の扱い方を覚えれば、プログラムの安定性を高めることにもつながるでしょう。

Javaのtry-catchとは?例外処理の方法をサンプルコードで解説

今回は、Javaの例外処理について具体例を交えながら説明していきます。例外の扱い方を覚えれば、プログラムの安定性を高めることにもつながるでしょう。

スキルアップ

2022/12/19 UP

Javaによるプログラミングでは、「例外」への適切な対処を求められる場面が少なくありません。その一方で、「例外は難しい概念だ」というイメージをもっている人もいるのではないでしょうか。

そこで今回は、Javaの例外処理について具体例を交えながら説明していきます。例外の扱い方を覚えれば、プログラムの安定性を高めることにもつながるでしょう。

なお、Javaについてより基本から知りたい場合は、こちらの記事もチェックしてみてください。
【入門】Javaとは?言語の特徴やプログラミングの流れをわかりやすく解説!

Javaで例外が発生する例

まずは、例外がどのように発生するのか、その様子を観察してみましょう。以下は、場合によって例外が発生する恐れのあるJavaのサンプルコードです。

サンプルコード:

public class Main {
    static void printSpeed(int meter, int seconds) {
        System.out.println(meter + "メートルを" + seconds + "秒で進むとき、");

        var speed = meter / seconds;
        System.out.println("速さは秒速" + speed + "メートルです。");
    }

    public static void main(String[] args) {
        Main.printSpeed(100, 10);

        System.out.println("プログラムを終了します。");
    }
}

実行結果:

100メートルを10秒で進むとき、
速さは秒速10メートルです。
プログラムを終了します。

プログラム中で、「距離」と「時間」から「速度」を計算して表示しているのがわかるでしょうか。また、プログラムを終了する際にもメッセージを1つ表示しています。

主要な処理は「printSpeed()」メソッドに収められていますが、それを呼び出しているのが次の部分です。

Main.printSpeed(100, 10);

では、第2引数で指定している「時間」の値をゼロにしたらどうなるでしょうか。

Main.printSpeed(100, 0);

実際にこの行を変更して実行してみると、以下のような結果が表示されます。

実行結果:

100メートルを0秒で進むとき、
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at Main.printSpeed(Main.java:5)
    at Main.main(Main.java:10)

これは、例外が発生したということです。また、終了時のメッセージが表示されていないことから、プログラムが最後まで実行されなかったとわかります。適切な対処をしていないため、例外が発生した時点でプログラムの実行が停止されたのです。

上記の実行結果には、例外の種類を表す情報も含まれているのがわかるでしょうか。ここでは、「ArithmeticException」が発生した例外の名前です。「/ by zero」は「ゼロによる除算」という意味で、整数の割り算で分母をゼロにしてしまったために例外が発生したのだとわかります。

Javaで対処できる例外の種類

ArithmeticExceptionも含め、Javaの例外はすべてThrowableのサブクラスとして定義されたクラスです。これらは、ErrorとExceptionの2種類に大別されます。

Errorとは

JavaのErrorは、「重大なエラー」を扱うためのクラスです。例えば、Javaの仮想マシンが意図せず停止したり、入出力で何か深刻な問題が発生したりといった状況を表します。

これらは通常、プログラム内で対処すべき状況ではありません。そのため、例外処理の対象外と考えるのが一般的です。

Exceptionとは

Exceptionは、直訳すると「例外」という意味です。Javaで「例外処理」といえば、通常はExceptionクラスに属する例外に対処することを指します。

上記のサンプルコードで確認されたArithmeticExceptionも、その名前からExceptionの一種だと推測できるでしょう。これにより、「これはプログラム内で対処できる例外だ」ということがわかるのです。

Javaにおける例外処理の基本

上記のサンプルコードを題材にして、ここからは例外処理の具体的な方法について説明していきます。

try-catchで例外を捕捉する

Javaのプログラム中で例外に対処するには、try-catchの記法を用います。まず、例外が発生する可能性がある部分をtryブロックで囲みましょう。次に、対処したい例外の種類をcatchブロックに記述し、例外発生時の処理を追加します。

以下は、ArithmeticExceptionが発生した場合にメッセージを表示するように修正したサンプルコードです。

サンプルコード:

public class Main {
    static void printSpeed(int meter, int seconds) {
        try {
            System.out.println(meter + "メートルを" + seconds + "秒で進むとき、");

            var speed = meter / seconds;
            System.out.println("速さは秒速" + speed + "メートルです。");
        }
        catch (ArithmeticException e) {
            System.out.println("算術演算の例外:" + e.getMessage());
        }
    }

    public static void main(String[] args) {
        Main.printSpeed(100, 0);

        System.out.println("プログラムを終了します。");
    }
}

実行結果:

100メートルを0秒で進むとき、
算術演算の例外:/ by zero
プログラムを終了します。

先ほど発生していた例外は「ゼロによる除算」だったので、割り算を行なっている行がtryブロックの内側にくるようにしました。そのうえで、ArithmeticExceptionに対応するcatchブロックを配置しています。

実行結果の表示内容から、割り算の行で例外が発生した時点でcatchブロックに制御が移ったことがわかるでしょう。ここで重要なのは、例外発生後もプログラムが動作を続行できている点です。これにより、終了時のメッセージまできちんと表示されています。

呼び出し元で例外を捕捉する

さきほどの例では、例外を発生したその場で処理していました。これと同様の処理を、以下のように呼び出し元で行なうことも可能です。

サンプルコード:

public class Main {
    static void printSpeed(int meter, int seconds) {
        System.out.println(meter + "メートルを" + seconds + "秒で進むとき、");

        var speed = meter / seconds;
        System.out.println("速さは秒速" + speed + "メートルです。");
    }

    public static void main(String[] args) {
        try {
            Main.printSpeed(100, 0);
        }
        catch (ArithmeticException e) {
            System.out.println("算術演算の例外:" + e.getMessage());
        }

        System.out.println("プログラムを終了します。");
    }
}

tryブロックで囲む対象が、メソッドの呼び出し元に変更されたのがわかるでしょうか。このように記述しても、メソッド内で例外が発生すればcatchブロックに制御が移ります。

ここでポイントとなるのは、たとえメソッドの呼び出しが何重にも行なわれた先で例外が発生したのだとしても、その例外を最初の呼び出し元に記述したcatchブロックで捕捉できるという点です。この仕組みのおかげで、プログラムの枝葉にあたる部分で発生した例外も、種類さえわかっていれば根元で対処することができます。

finallyで終了処理を記述する

Javaのtry-catchには、オプションで記述できるfinallyブロックという記法があります。具体的には、以下のサンプルコードのようなものです。

サンプルコード:

public class Main {
    static void printSpeed(int meter, int seconds) {
        System.out.println(meter + "メートルを" + seconds + "秒で進むとき、");

        var speed = meter / seconds;
        System.out.println("速さは秒速" + speed + "メートルです。");
    }

    public static void main(String[] args) {
        try {
            Main.printSpeed(100, 0);
        }
        catch (ArithmeticException e) {
            System.out.println("算術演算の例外:" + e.getMessage());
        }
        finally {
            System.out.println("処理完了。");
        }

        System.out.println("プログラムを終了します。");
    }
}

実行結果:

100メートルを0秒で進むとき、
算術演算の例外:/ by zero
処理完了。
プログラムを終了します。

実行結果の表示内容から、try-catchの処理が終わる際にfinallyブロックの内側が実行されていることがわかるでしょう。tryブロック中で例外が発生したかどうかにかかわらず、try-catchの最後にはfinallyブロックが実行されます。

finallyブロックは、tryブロック内で使用したリソースを確実に解放したい場合に便利です。メモリやファイルなどのリソースを確保するプログラムでは、途中で例外が発生すると解放のタイミングを失ってしまうケースもあるかもしれません。リソースを解放する処理をfinallyブロック内に記述しておけば、そのような事態は避けられるでしょう。

Javaで複数の例外を処理するには

プログラム中で複数の例外が発生するかもしれないとしたら、どのように処理すればよいのでしょうか。ここからは下記のサンプルコードを題材として、2つ以上の例外にまとめて対処する方法について説明していきます。

サンプルコード:

public class Main {
    static class Person {
        String name;
        String loves;

        Person(String name, String loves) {
            this.name = name;
            this.loves = loves;
        }
    }

    public static void main(String[] args) {
        var persons = new Person[3];
        persons[0] = new Person("太郎", "犬");
        persons[1] = new Person("花子", "猫");

        for (int i=0; i<=persons.length; i++) {
            var name = persons[i].name;
            var loves = persons[i].loves;
            System.out.println("私は" + name + "、" + loves + "が好きです。");
        }

        System.out.println("プログラムを終了します。");
    }
}

これは、「名前」と「好きなもの」を配列に格納しておき、その内容をfor文で順番に取り出して表示していくプログラムです。しかし、実行すると2種類の例外が発生します。

なお、配列とfor文については、それぞれ詳しい記事があるので併せて参考にしてください。

Javaの配列とは?Arrayの使い方とサンプルを紹介

【Java入門】whileとdo whileの使い方 - ループ処理の注意点やforとの使い分けを解説

1つ目の例外を捕捉する

では、サンプルコードを実行して結果を確認してみましょう。

実行結果:

私は太郎、犬が好きです。
私は花子、猫が好きです。
Exception in thread "main" java.lang.NullPointerException: Cannot read field "name" because "persons[i]" is null
    at Main.main(Main.java:18)

表示内容から、「NullPointerException」という名前の例外が発生していることがわかります。このサンプルコードでは2種類の例外が発生すると説明しましたが、1つ目の例外で実行が停止されてしまったので、現時点では2つ目については情報がありません。

「Cannot read field "name" because "persons[i]" is null("persons[i]"がnullのため"name"フィールドを読み取れない)」というメッセージから、配列「persons」のいずれかの要素がnullになっているのだと推測できます。プログラムを詳しく調べれば、配列のサイズが3なのに対し、要素が2つしか格納されていないことに気付くでしょう。

var persons = new Person[3];
persons[0] = new Person("太郎", "犬");
persons[1] = new Person("花子", "猫");

これが原因となり、3つ目の要素にアクセスを試みた段階でnullポインタの例外が発生しているのです。

例外が発生しないようにプログラムを修正する方法も考えられますが、ここではtry-catchで対処することにしましょう。

for (int i=0; i<=persons.length; i++) {
    try {
        var name = persons[i].name;
        var loves = persons[i].loves;
        System.out.println("私は" + name + "、" + loves + "が好きです。");
    }
    catch (NullPointerException e) {
        System.out.println("nullポインタの例外:" + e.getMessage());
    }
}

try-catchの書き方は、例外の種類が違う以外はArithmeticExceptionのときと同様です。例外の発生位置をtryブロックで囲み、catchブロックでNullPointerExceptionを捕捉します。

2つ目の例外を捕捉する

NullPointerExceptionに対処できたので、もう一度プログラムを実行してみましょう。

実行結果:

私は太郎、犬が好きです。
私は花子、猫が好きです。
nullポインタの例外:Cannot read field "name" because "persons[i]" is null
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
    at Main.main(Main.java:19)

NullPointerExceptionに対応するcatchブロックでプログラムが実行を続けられるようになり、2つ目の例外「ArrayIndexOutOfBoundsException」が発生しました。

「Index 3 out of bounds for length 3(インデックス3は長さ3の範囲外)」と表示されていることから、配列にアクセスする際のインデックスに問題がありそうだと推測できます。プログラムを調べれば、配列の有効なインデックスが0〜2なのに対し、for文では0〜3の範囲でループしていると気付くでしょう。

for (int i=0; i<=persons.length; i++) {
    ・・・
}

これが原因となり、配列の範囲を越えたアクセスで例外が発生しているのです。

ここでも、例外処理で対処することにします。先ほど記述したtry-catchがあるので、次のようにcatchブロックを追加しましょう。

for (int i=0; i<=persons.length; i++) {
    try {
        var name = persons[i].name;
        var loves = persons[i].loves;
        System.out.println("私は" + name + "、" + loves + "が好きです。");
    }
    catch (NullPointerException e) {
        System.out.println("nullポインタの例外:" + e.getMessage());
    }
    catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("配列インデックスの例外:" + e.getMessage());
    }
}

このように、catchブロックは必要なだけ記述することが可能です。

では、もう一度プログラムを実行してみましょう。

実行結果:

私は太郎、犬が好きです。
私は花子、猫が好きです。
nullポインタの例外:Cannot read field "name" because "persons[i]" is null
配列インデックスの例外:Index 3 out of bounds for length 3
プログラムを終了します。

すべての例外への対処が完了し、プログラムを最後まで実行できるようになりました。

例外をまとめて捕捉する

例外の種類が複数あるときは、まとめて捕捉するとプログラムをコンパクトにできる場合があります。以下は、2種類の例外に対処するcatchブロックの例です。

for (int i=0; i<=persons.length; i++) {
    try {
        var name = persons[i].name;
        var loves = persons[i].loves;
        System.out.println("私は" + name + "、" + loves + "が好きです。");
    }
    catch (NullPointerException|ArrayIndexOutOfBoundsException e) {
        System.out.println("nullポインタまたは配列インデックスの例外:" + e.getMessage());
    }
}

実行結果:

私は太郎、犬が好きです。
私は花子、猫が好きです。
nullポインタまたは配列インデックスの例外:Cannot read field "name" because "persons[i]" is null
nullポインタまたは配列インデックスの例外:Index 3 out of bounds for length 3
プログラムを終了します。

1つのcatchブロックに、NullPointerExceptionとArrayIndexOutOfBoundsExceptionの2種類の例外を指定している様子がわかるでしょう。この記法では、指定したどちらの例外でも捕捉できますが、どちらにも該当しない例外は対象外となります。

より広範囲の例外をまとめて捕捉したい場合は、次ような書き方も可能です。

for (int i=0; i<=persons.length; i++) {
    try {
        var name = persons[i].name;
        var loves = persons[i].loves;
        System.out.println("私は" + name + "、" + loves + "が好きです。");
    }
    catch (Exception e) {
        System.out.println("例外:" + e.getMessage());
    }
}

実行結果:

私は太郎、犬が好きです。
私は花子、猫が好きです。
例外:Cannot read field "name" because "persons[i]" is null
例外:Index 3 out of bounds for length 3
プログラムを終了します。

このようにすれば、Exceptionに該当する例外ならどれでも捕捉可能です。Javaのプログラム内で対処が必要な例外はいずれもExceptionクラスを親とするため、1つのcatchブロックで事実上すべての例外に対処できる方法だといえます。

Javaで例外を発生させるには

ここまでは発生した例外に対処する方法について説明してきましたが、Javaの例外は自分で発生させることも可能です。

既存のExceptionを使う

プログラムが高度になってくると、エラーケースなどを例外として扱いたくなることもあるかもしれません。そのような場合には、throw文を使用すれば自分で例外を発生させられます。

次のサンプルコードのように、すべての例外の親であるExceptionクラスを使う方法が手軽でしょう。

サンプルコード:

public class Main {
    public static void main(String[] args) {
        try {
            throw new Exception("例外を発生させました!");
        }
        catch (Exception e) {
            System.out.println("例外:" + e.getMessage());
        }

        System.out.println("プログラムを終了します。");
    }
}

実行結果:

例外:例外を発生させました!
プログラムを終了します。

tryブロックの内側でExceptionクラスのインスタンスを作成し、throwに引き渡している様子がわかるでしょうか。こうして自分で発生させた例外も、ほかの例外と同様catchブロックで捕捉できます。

独自のExceptionを定義する

自分で発生させた例外を、ほかの例外と区別してcatchブロックを記述したい場合も考えられます。そのためには、新しい種類の例外が必要です。

具体的には、次のサンプルコードのようにして独自の例外クラスを定義します。

サンプルコード:

public class Main {
    static class MyException extends Exception {
        MyException(String message) {
            super(message);
        }
    }

    public static void main(String[] args) {
        try {
            throw new MyException("例外を発生させました!");
        }
        catch (MyException e) {
            System.out.println("独自の例外:" + e.getMessage());
        }

        System.out.println("プログラムを終了します。");
    }
}

実行結果:

独自の例外:例外を発生させました!
プログラムを終了します。

ここでは、例外クラス「MyException」を、Exceptionのサブクラスとして定義しているのがポイントです。これにより、独自の例外を既存の例外と同様にthrowできるようになります。

また、catchブロックに「MyException」を指定すれば、ほかの例外と区別して独自の例外のみを捕捉可能です。

Javaのtry-catchで例外に強いプログラミングを

Javaによるプログラミングでは、さまざまな理由から発生する例外を目にする機会が少なくありません。何も対処しなければ、プログラムは例外が発生した時点で停止してしまいます。

安定して動作するプログラムを実現するには、例外処理が大切です。発生する例外の種類に応じた処理を、try-catchで記述しましょう。必要があれば、新しい種類の例外を独自に定義して利用することも可能です。