第8章 例外処理

8.1 例外とエラーについて

例外とはプログラム実行時に発生する予期せぬエラーのことを言います。Javaプログラムでは発生した例外の内容により「エラー(Error)」と「例外(Exception)」に区別され、その違いはプログラムで対処できるかできないかにあります。「エラー」の場合はプログラムで対処できない致命的な例外を指し、「例外」の場合はプログラムで対処できる例外を指します。
エラーの例としては、「ハードウェアの故障」、「メモリ不足」などが挙げられます。
例外の例としては以下が挙げられます。

  • 整数を0で割り算を行った。
  • 配列の要素数より大きい要素数を指定してアクセスを行った。
  • ユーザーが入力間違いを行った。(数値入力を要求しているのに、英字の入力等)
  • 存在しないファイルを指定し、ファイルの読み込みを行った。
  • データベースに接続が行えなかった。
  • 割り当てられていない記憶領域へのアクセスを行った。(不正な値のポインタを参照等)

上記は、プログラム(ソースコード)をコンパイルするときには見つけられない誤りです。プログラムを実行して初めてエラーがあることが分かります。Javaでは、このような実行時のエラーを適切に処理するために、例外(exception:エクセプション)という仕組みを備えています。
まずは実行時のエラーがどのようなものなのか、次の項のプログラムで確認してみましょう。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ArrayException1
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ArrayException1 {
    public static void main(String[] args) {
      // int型配列を定義
      int[] intArray = new int[5];

      // 添え字10に値を代入
      System.out.println("intArray[10]に数値を代入します。");
      intArray[10] = 50; // 最大配列要素数を超えた代入処理

      // 結果を表示
      System.out.println("intArray[10]に50を代入しました。");
      System.out.println("処理終了");
    }
  }
  
実行結果

解説
実行結果からも分かるように、処理の途中でエラーが発生しプログラムが終了しています。それは10行目の処理で、わざと最大配列要素5を超えた配列要素10に値の代入を行っている為です。

10行目でエラーになりプログラムが終了してしまったため、それ以降の13行目の処理は行われません。
実行結果からもメッセージが出力されていないのが確認できます。


図 8.1.1: 実行時エラーの情報

① コンソールにメッセージが出力されているので、9行目までの処理は正しく行われている。
② 例外の種類:ArrayIndexOutOfBoundsException(範囲外の添字で配列にアクセス)
③ 例外の詳細:10のインデックス(添字)にアクセスした。
④ 例外発生ファイル:jp.co.f1.basic.ch08.ArrayException1.main(パッケージ+クラス名+メソッド名)
⑤ 発生箇所:ArrayException1.javaファイルの10行目で発生。
上記の情報を元にプログラムのエラーを確認することができます。

Javaではこのようなエラーが起こることを、「ArrayIndexOutOfBoundsExceptionという種類の例外が起きた」といいます。「例外が起きた」ということを「 例外が発生した」または「例外が送出(throw・スロー)された」と呼ぶこともあります。

ポイント
  • 実行時エラー(例外)が発生すると、処理が中断してしまう。その際コンソール画面にエラーの詳細が表示されるので、それを元にエラー原因を知ることができる。

では次の項で今回発生したような実行時エラー(例外)を処理するための基本構文を紹介します。

8.1.2 例外を処理するための仕組み(try-catch)

実行時エラー(例外)に対して何も処理を行わないと、プログラムの途中で処理が中断してしまうことが確認できました。実行時エラーに対して適切な処理を行うコードを記述してみましょう。このような処理のことを「例外処理(exception handling)」といいます。
以下に例外処理の基本的な構文を示します。

書式:例外処理の基本

凡例:例外処理の基本

例外処理を記述して2つのブロック(tryとcatch)をつけると、次のような順番で例外が処理されます。

① tryブロック中で例外が起きると、そこで処理を中断し、発生した例外に対応するcatchブロックを探す。
② 例外がcatchブロックの()内の例外の種類と一致していれば、そのcatchブロック内の処理を行う。
③ catchブロック内の処理が終わったら、そのtry~catchブロックの後の処理を続ける。

それでは実際に上記①~③のようになるか、次項のサンプルで確認してみましょう。
前項のサンプルに例外処理を追加したものを紹介します。

8.1.3 配列要素数を超えたアクセスに対して例外処理を行うプログラム

配列にわざと最大配列要素の数を超えて代入を行い、実行時エラーを起こし例外処理が行われることを確認します。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ArrayException2
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ArrayException2 {
    public static void main(String[] args) {
      try {
        // int型配列を定義
        int[] intArray = new int[5];

        // 添え字10に値を代入
        System.out.println("intArray[10]に数値を代入します。");
        intArray[10] = 50; // 最大配列要素数を超えた代入処理

        // 結果を表示
        System.out.println("intArray[10]に50を代入しました。");
      } catch (ArrayIndexOutOfBoundsException e) { // 最大配列要素数越えの例外処理
        System.out.println("配列の要素数を超えています。");
      }
      System.out.println("処理終了");
    }
  }
  
実行結果

解説

例外が発生しそうな部分の6行目~14行目をtryブロック(5~15行目)で囲んでいます。

  try{
    //int型配列を定義
    int[] intArray = new int[5];

    //添え字10に値を代入
    System.out.println("intArray[10]に数値を代入します。");
    intArray[10] = 50;	//最大配列要素越えた代入処理

    //結果を表示
    System.out.println("intArray[10]に50を代入しました。");
  }catch(ArrayIndexOutOfBoundsException e){ //最大配列要素数を超えた例外処理
  

11行目で配列要素数を超えて値を代入しているため、例外ArrayIndexOutOfBoundsExceptionが発生します。

  intArray[10] = 50; //最大配列要素数を越えた代入処理
  

続いて15行目~17行目にてtryブロック内で発生する、例外の種類ArrayIndexOutOfBoundsExceptionを指定してcatchブロックを設定しています。この記述により11行目で発生した例外を処理できるようになり、処理を途中で中断する代わりに16行目の処理を行い画面にメッセージを出力します。

  }catch(ArrayIndexOutOfBoundsException e){ //最大配列要素を超えた例外処理
    System.out.println("配列の要素数を超えています。");
  }
  

try~catchブロックの後から処理が再開し、18行目のメッセージを出力していることが確認できます。

  System.out.println("処理終了");
  

以下の図8.1.2に今回のプログラム処理の流れイメージを示します。

  package jp.co.f1.basic.ch08;

  public class ArrayException2 {
    public static void main(String[] args) {
      try{
        //int型配列を定義
        int[] intArray = new int[5];

        //添え字10に値を代入
        System.out.println("intArray[10]に数値を代入します。"); //①
        intArray[10] = 50;

        //結果を表示
        System.out.println("intArray[10]に50を代入代入しました。");
      }catch(ArrayIndexOutOfBoundsException e){
        System.out.println("配列の要素数を超えています。"); //②
      }
      
      System.out.println("処理終了"); //③
    }
  }
  


図 8.1.2:ArrayException2例外処理の流れイメージ

発生した例外とcatchブロック()内の例外の種類が一致し、catchブロックの処理が行われることを「catchブロックで例外を受け取る」や「例外をキャッチ(catch)する」と呼びます。図8.1.2を見ると分かるように、例外処理を行うと、8.1.1項のサンプルのようにプログラムが途中で終了することがありません。catchブロック内の処理が行われ、最後まで問題なく処理が実行されているのが分かります。その結果、エラーに対処したプログラムになっていることが確認できます。
それではもう一つ実行時エラーを確認し、そのエラーに対して例外処理を行うプログラムを紹介します。

8.1.4 0除算に対して例外処理を行わないプログラム

数値を0で除算し実行時エラーをわざと起こすプログラムです。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ZeroException1
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ZeroException1 {
    public static void main(String[] args) {
      // 数値を0で除算します。
      int num = 10 / 0;

      // 結果を表示
      System.out.println("10/0の結果は" + num);
      System.out.println("処理終了");
    }
  }
  
実行結果

解説

プログラミングでは「0で除算」することができないようになっていますが、6行目でわざと0除算をおこなっています。

  int num = 10 / 0;
  

実行結果を見ても分かるように、6行目で実行時エラーになり例外が送出されているのが確認できます。
その時に発生する例外は算術計算での例外「ArithmeticException(アリスメティックエクセプション)」となります。
では次項でこのソースコードに例外処理を追加したサンプルを紹介します。

8.1.5 0除算に対して例外処理を行うプログラム

数値を0で除算し実行時エラーをわざと起こし、例外処理を行うプログラムです。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ZeroException2
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ZeroException2 {
    public static void main(String[] args) {
      try {
        // 数値を0で除算します。
        int num = 10 / 0;

        // 結果を表示
        System.out.println("10/0の結果は" + num);
      } catch (ArithmeticException e) { // 0除算の例外処理
        System.out.println("0で除算はできません。");
      }
      System.out.println("処理終了");
    }
  }
  
実行結果

解説

例外が送出される処理(この処理では7行目)に対してtryブロック(5行目~11行目)で囲みます。

  try {
    //数値を0で除算します。
    int num = 10 / 0;

    //結果を表示
    System.out.println("10/0の結果は" + num);
  } catch (ArithmeticException e) { //0除算の例外処理
  

発生する例外の種類(クラス)をcatchの()内に記述しています。今回はArithmeticExceptionとなります。
12行目では例外をキャッチした時に行わせたい、メッセージ表示の処理を行っています。

  } catch (ArithmeticException e) {		//0除算の例外処理
    System.out.println("0で除算はできません。");
  }
  

実行結果からも確認できるように、前項のサンプルとは違い途中で処理が中断することなく最後まで動作していることが確認できます。

2つのプログラムをもとに例外処理の基本を紹介しましたが、本来わざとエラーを起こすプログラムを組むことはありません。例外処理の基本的な考え方は、作成したプログラムが意図しない原因でプログラムを途中終了させてしまわないようにすることです。基本的にJavaでは、例外が発生する可能性がある箇所では、必ず例外処理を記述しなければいけないことを覚えておきましょう。例外が発生することを想定し例外処理を組み込んでおけば、エラーに強いプログラムを作成できることにもなります。

ポイント
  • 例外が発生する可能性がある箇所には例外処理(try~catch)を必ず記述する。そして例外処理を行うことで、エラーに強いプログラムを作成することが可能になる。

8.1.6 例外処理(try~catch)の動作について

例外処理を行っていないプログラムは途中で終了してしまうことは理解できたと思います。しかし、正確に言うと少し話が違ってきます。
これまで見てきたmainの中でメソッドを使わずにシンプルなプログラムの場合、「実行時エラー(例外)が起こる処理を記述していると、そのエラーの箇所で途中終了になる」という仕組みを理解しました。
では呼び出し元のメソッドに呼ばれるケースを考えてみます。エラーに対する例外処理がそのメソッド内で見つからない場合には、呼び出し元のメソッドに戻って対応する例外処理を探す仕組みになっています。
本章でこれまで紹介したプログラムではmainメソッドで例外が発生していました。結果それ以上呼び出し元のメソッドに戻ることができず、例外処理を行わなかった場合はプログラムが途中で終了していた理由になります。

例外処理を行わないプログラムの流れ

図 8.1.1: 実行時エラーの情報

次項では、mainメソッドから呼び出す別のメソッド内で例外が発生した場合の例外処理のプログラムを紹介します。

8.1.7 呼び出したメソッド内で発生した例外の処理を行わないプログラム

mainメソッドではなく別メソッドで0除算をわざと行い、その例外処理を行わない場合の流れを確認します。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ThrowZeroException1
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ThrowZeroException1 {
    static public void calcTest() {
      // 数値を0で除算します。
      int num = 10 / 0;

      // 結果を表示
      System.out.println("10/0の結果は" + num);
    }

    public static void main(String[] args) {
      // calcTestメソッド呼び出し
      calcTest();

      System.out.println("処理終了");
    }
  }
  

実行結果
ThrowZeroException1.java ※例外処理なし

解説
calcTest()メソッド内の処理6行目で0除算を行い、例外をわざと発生させています。

実行結果を見ると、例外の箇所が2箇所になっているのが確認できます。0除算例外の発生が呼び出し先のcalcTest()メソッドでもmain()メソッドまで遡っています。

① calcTest メソッドで例外が発生している箇所(実際の計算部分)
② mainメソッド内で例外を起こしている処理(メソッド呼び出し部分)
今回のプログラムの処理の流れを表すと、以下に示す図の流れになります。


図 8.1.4:呼び出し元に例外処理が用意されていない場合の処理の流れ

呼び出し先から例外が発生した場合main()メソッドへ処理が遡っていることは説明しましたが、本当にそうなのかを確認してみましょう。今回のプログラムのmain()メソッド側に例外処理を追加したプログラムを使い次の項で説明を行います。

8.1.8 呼び出したメソッド内で発生した例外の処理を呼び出し元で行うプログラム

mainメソッドではなく別メソッドで0除算をわざと行い、呼び出し元で例外処理を行った時の流れを確認します。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ThrowZeroException2
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ThrowZeroException2 {
    public static void calcTest() {
      // 数値を0で除算します。
      int num = 10 / 0;

      // 結果を表示
      System.out.println("10/0の結果は" + num);
    }

    public static void main(String[] args) {
      try {
        // calcTestメソッド呼び出し
        calcTest();
      } catch (ArithmeticException e) { // 0除算の例外処理
        System.out.println("0で除算はできません。");
      }
      System.out.println("処理終了");
    }
  }
  

実行結果
ThrowZeroException2.java ※例外処理あり

解説
calcTestメソッド内には特に例外処理は追加していませんが、mainメソッド内でtry~catch文を記述しています。これによりcalcTest()メソッド内で例外が発生してもmain()メソッドで例外を処理することができます。

  try {
    //calcTestメソッド呼び出し
    calcTest();
  } catch (ArithmeticException e) {	//0除算の例外処理
    System.out.println("0で除算はできません。");
  }
  System.out.println("処理終了");
  

今回のプログラムの処理の流れを表すと、以下に示す図の流れになります。


図 8.1.5: 呼び出し元に例外処理が用意されている場合の処理の流れ

0除算を行っている箇所はcalcTest()メソッド内で、例外を処理している箇所はmainメソッドと別になっていますが、途中で終了することなく正しく処理が終了しています。この結果から例外の流れが呼び出し先から呼び元へ遡っていることが確認できました。
今回main()メソッドで例外を処理しましたが、もちろんcalcTest()メソッドで行っても問題ありません。作成するプログラムによっては、例外が発生したときに呼び出し元まで遡られても困るケースもあります。
例外をどこに組み込む必要があるのかは、作成するプログラムによって大きく変わりますので注意してください。

ポイント
  • 例外が発生した場合、例外処理が行われているかのチェックが呼び出し先から呼び出し元へと遡っていく。

8.1.9 例外処理のfinallyブロックについて

例外処理では、例外が発生したメソッド内でcatchブロックが見つからなかった場合に、呼び出し元のメソッドに戻ってcatchブロックが探されることになっています。このような場合、例外の発生に関わらず、そのメソッド内で必ず行っておきたい重要な処理がある場合どうすればいいのでしょうか。その答えは「finallyブロック」を利用することで解決できます。
それでは以下に例外処理のfinallyブロックを追加した構文を示します。

書式:例外処理とfinallyブロック

finallyブロックはcatch文の次に記述します。

凡例:例外処理とfinallyブロック

finallyブロックを追加することで、tryブロック内で例外の有無に関わらず必ず行わせたい処理を設定することができます。finallyブロックを利用していないと、例外が発生したときに、重要な処理が飛ばされたままプログラムの処理が進んでしまうと困るような場合に大変有用な機能といえます。但し絶対使用しないといけない訳ではないので省略可能になっています。

try~catch~finally処理の流れ
try~catch~finallyブロック内の、処理の流れを以下の図に示します。


図 8.1.6: try~catch~finally処理の流れ

ポイント
  • 例外処理にfinallyブロックをつけると、例外の有無に関わらず必ず処理を行ってくれる。(※省略も可能)

これまでのプログラムではcatchブロックは1つだけ記述していましたが、1つだけではなく複数記述することも可能です。複数記述した場合は上のブロックから順にチェックを行います。また、finallyブロックを記述しているとcatchブロックを省略することもできます。
これまでの例外処理の仕組みをまとめると以下になります。

  • tryブロックの後には1つ以上(複数可)のcatchブロックまたは、1つのfinallyブロックを記述する。
  • catchの括弧の中で適合する例外クラスが見つかれば、そのブロック内の処理のみ実行する。
  • finallyブロックは例外の発生有無に関係なく必ず実行される。

では次項でfinallyブロックを使った例外処理のプログラムを紹介します。

8.1.10 例外処理のfinallyブロックを利用したプログラム

例外処理にfinallyブロックを追加して、正しくそのfinallyブロック内の処理が行われることを確認します。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: FinallyBlock
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class FinallyBlock {
    public static void main(String[] args) {
      try {
        // int型配列を定義
        int[] intArray = new int[5];

        // 配列に値を代入
        System.out.println("配列に数値を代入します。");
        intArray[10] = 50; // 例外をわざと発生させる代入
        // intArray[0] = 50; //正しく代入する

        // 結果を表示
        System.out.println("配列に50を代入しました。");
      } catch (ArrayIndexOutOfBoundsException e) { // 最大配列要素数を超えた例外処理
        System.out.println("配列の要素数を超えています。");
      } finally {
        System.out.println("例外処理の最後の処理です。"); // finallyブロック
      }
      System.out.println("処理終了");
    }
  }
  

実行結果
※例外が発生した場合(サンプルソースのまま実行してください)

※例外が発生しなかった場合(実行する場合11行目をコメントアウト、12行目のコメントを解除してください)

解説

11行目の処理は最大配列要素を超えた代入を行い、例外をわざと発生させる処理を記述しています。
12行目は配列要素の有効範囲内に代入を行っています。

  intArray[10] = 50;    //例外をわざと発生させる代入
  //intArray[0] = 50;    //正しく代入する
  

実行結果から確認できますが、例外が発生した場合もしなかった場合もfinallyブロック内で設定した、19行目のメッセージ出力処理が行われていることが確認できます。

  }finally{
    System.out.println("例外処理の最後の処理です。");
  }
  

では次の項でcatchブロックを複数使ったプログラムを紹介します。

8.1.11 例外処理に複数のcatchブロックを利用したプログラム

複数のcatchブロックを設定して該当する例外が発生した場合に、正しくそのcatchブロック内の処理が行われることを確認します。

① ソース・フォルダー: myproj_basic/src
② パッケージ: jp.co.f1.basic.ch08
③ 名前: ManyCatch
④ 作成するメソッド・スタブの選択:public static void main(String[] args) にチェックを入れる

  package jp.co.f1.basic.ch08;

  public class ManyCatch {
    public static void main(String[] args) {
      try {
        // int型配列を定義
        int[] intArray = new int[5];

        // 配列に値を代入
        System.out.println("配列に数値を代入します。");
        intArray[10] = 50; // 例外をわざと発生させる代入
        // intArray[0] = 50; //正しく代入する
        System.out.println("配列に50を代入しました。"); // 結果を表示

        int num = intArray[0] / 0; // 例外をわざと発生させる0除算
        System.out.println("割り算の結果は" + num + "です。");

      } catch (ArrayIndexOutOfBoundsException e) { // 最大配列要素数を超えた例外処理
        System.out.println("配列の要素数を超えています。");
      } catch (ArithmeticException e) { // 0除算の例外処理
        System.out.println("0で除算はできません。");
      } finally { // finallyブロック
        System.out.println("例外処理の最後の処理です。");
      }
      System.out.println("処理終了");
    }
  }
  

実行結果
※例外が発生した場合(サンプルソースのまま実行してください)

※0除算の例外が発生した場合(実行する場合11行目をコメントアウト、12行目のコメント解除してください)

解説
18行~22行目のようにcatchブロックは複数指定することが可能です。
2つの実行結果からも分かるように、該当する例外が発生すると、その例外クラスを指定しているcatchブロック内の処理が正しく行われていることが確認できます。

  }catch(ArrayIndexOutOfBoundsException e){ 		//最大配列要素を超えた例外処理
    System.out.println("配列の要素数を超えています。");
  }catch(ArithmeticException e){ 			//0除算の例外処理
    System.out.println("0で除算はできません。");
  }
  
例外処理のパターン

これまでのプログラムではいくつかの例外処理のパターンを見てきました。本項までで紹介したプログラムでは以下に示す①~③のパターンは学習しました。例外処理ではcatchブロックを省略することも可能ですが、その際はfinallyブロックを記述する必要があります。例外処理はtryブロックと他のブロック(catch、finally)を組み合わせて使用しなければならないルールがあるからです。プログラム例として④は紹介しませんがこのような使い方もあることを覚えておいて下さい。

① try-catch(必須)
② try-catch-finally(catchがある場合は任意設定)
③ try-(catch*任意の数)-finally(catchがある場合は任意設定)
④ try-finally(必須)

ポイント
  • 例外処理はcatchブロックを複数記述することができる。逆にcatchブロックは省略も可能だが、tryブロックだけでは例外処理は記述できないのでその際はfinallyブロックを記述する必要がある。

NEXT>> 8.2 例外処理の仕組みまとめ