プログラミング入門(金曜1・2限)講義資料(2008年後期)

プログラミング入門(例外処理)

プログラム実行中のエラーについて

プログラムの実行中に,プログラムが予想していなかった事態が起きることがあります.

例えば,ネットワーク経由で数値を受け取る処理を考えます(別にキーボードからでも構わないのですが).最初に文字列として受け取って,IntegerクラスのparseIntメソッドでint型に変換することになります.ここで,もし届いた文字列が数字ではなくてアルファベットだったらどうなるでしょうか?

あるいは,ネットワークを使うプログラムで,java.io.BufferedReaderのreadLineメソッドで相手からの応答を待っているときに,相手側のプログラムが突然終了してしまった場合(あり得ない事態ではあり得ません),どうするべきでしょうか? 他にも,ゼロで除算してしまったら? 等,プログラム実行中にはいろいろと想定していない事態が発生する可能性があります.

parseIntは文字列を受け取って数値を返すメソッドです.例えば,異常事態が起きたら特別な値(-1とか)を返す方法も考えられますが,これだと-1を受け取ることができませんし,毎回parseIntを呼び出すたびに-1と等しいかどうかチェックするのも大変です.

あるいは,毎回parseIntを呼び出す前に,渡す文字列が数字かどうかをチェックするという方法も考えられますが,これも大変です.

Javaでは,このため例外(Exception)というメカニズムを提供しています.

例外の概念

例外とは,上で述べたような異常事態の発生を通知するための仕組みです.

このような異常事態が発生したことを検知したメソッド(例えば parseInt) は例外を「投げ」ます.これによって異常事態が発生したことを知らせます.Javaの用語では,「投げ」ることを「スロー(throw)する」と言います.

異常事態が発生したメソッドを呼び出した側のメソッドでは,投げられた例外を「捕える」ことができます.捕まえれば,適切な処理を行うことができます.(同じく捕まえることを「キャッチ(catch)する」と言います).

あるいは捕まえるかわりに,さらに上位のメソッドに例外を「投げ」て,上位のメソッドに処理を委ねるという方法もあります.

例えば,メソッドAがメソッドBを呼び出し,メソッドBがメソッドCを呼び出している場合を考えます.メソッドCで何か異常事態が発生して例外を投げたとしましょう.メソッドBがそれを捕まえなかった場合はメソッドAが捕まえることができます.

Javaでの例外

さて,異常事態もいろいろな場合が考えられます.捕まえた方は,発生した異常事態に応じた処理をしたいでしょうから,さまざまな例外を区別する仕組みが必要です.Javaでは,例外は例外クラスのオブジェクトで表すことになっています.

それぞれのメソッドがどのような例外を発生するかは,APIドキュメントに書いてあります.例えば,IntegerクラスのparseIntの解説には,以下のように書いてあります.

public static int parseInt(String s)
                    throws NumberFormatException
...

例外:
  NumberFormatException - 文字列が解析可能な整数型を含まない場合

これは,渡された文字列が数字ではなかった場合,NumberFormatExceptionという例外が発生することを意味しています.NumberFormatExceptionはNumberFormatExceptionという例外クラスの1つです.

さて,例外が発生する可能性があるメソッドを呼び出す場合,基本的には以下のどちらかの処理を行う必要があります.

例外を捕まえる

例外を捕まえるには,try文catch文を使います.以下はNumberFormatExceptionを捕まる例です.

// キーボードから文字列を読み込む
String x = Keyboard.stringValue();
int i;
try {
    // ここで NumberFormatExceptionが投げられる可能性がある
    i = Integer.parseInt(x);
    System.out.println("i = " + i);
} catch (NumberFormatException e) {
    // NumberFormatExceptionが投げられたらここで捕まえる.
    // e には例外の情報が入っている.(eは変数なのでeという名前である必要はない)
    System.out.println("ちゃんと数字を入れてください!");
}

例外を捕まえることができるのは,try文の { } で囲まれた範囲だけです.

あるメソッドの中で例外が投げられると,それ以降の文は実行されません.上の例では,Integer.parseIntの中で例外が投げられたら,その次のSystem.out.printlnは実行されません.その代わりcatch文に実行が移ります.

catch文は,例外を捕まえたときにどのような処理をするかを書きます.上の例では投げられた例外クラス(ここではNumberFormatException)のオブジェクトが e に入ってきます.

捕まえたい例外の種類に応じて複数の catch 文を書くこともできます.

try {
    ...
}
catch (NumberFormatException e) {
    ...
}
catch (ArithmeticException e) {
    ...
}

細かい違いは無視して,全部の例外を捕まえたいという場合もあり得ます.こういうときは,catch (Exception e) のように書きます.これで全ての例外を捕まえることができます.

詳しくは説明しませんが,例外クラスは,java.lang.Exceptionというクラスを継承して作ることになっているので,こういう風にかけるのです.

例外を捕まえることで,異常な事態が起きてもそこから回復することができます(回復できるかどうかはどのような事態が発生したかによりますが).例えば,電卓プログラムで0で割り算してもプログラム全体がエラーで停止しないようにできます.

上位のメソッドに投げる

上位のメソッドに投げる場合は,メソッドを定義するときに,「このメソッドがどのような例外を投げる可能性があるか」を宣言する必要があります.それは,throwsを使って次のように書きます.

public class XXX {
   public aMethod() throws ExceptionA, ExceptionB {
      ...;
      例外ExceptionAを投げる可能性があるメソッドの呼び出し;
      ...;
      例外ExceptionBを投げる可能性があるメソッドの呼び出し;
      ...;
   }
}

ただし,いちいち投げる可能性がある全ての例外を書くのは大変なので,RuntimeExceptionのサブクラス(RuntimeExceptionから継承されたクラスのこと)は特に throws で宣言しなくても良いことになっています.(RuntimeExceptionのAPIドキュメントの「直系の既知のサブクラス」にリストされているような例外がそれにあたります.)

また,細かい違いは無視して「このメソッドは何かしら例外を投げる」と宣言する方法もあります.それには,throws Exceptionと書きます.

以前にファイルを取り扱った際,java.io.BufferedReaderクラスのreadLineメソッドを使いました.java.io.BufferedReaderのAPIドキュメントをみると,readLineメソッドはjava.io.IOExceptionという例外を投げる可能性があることがわかります.

readLine を使うメソッドは例外なく throws Exception と書いていたことを思い出してください.java.io.IOExceptionはRuntimeExceptionのサブクラスではないので,try文を使って例外を捕まえない限りは上位のメソッドに例外を投げることを宣言しないといけなかったのです.

例外が投げられて,最終的にmainメソッドでもその例外が捕らえられなかった場合,プログラムはエラーで終了します.

その他

アプレットで一定時間プログラムの実行を停止するとき,Threadクラスのsleepメソッドを使いました.このメソッドはInterruptedExceptionという例外が発生する可能性があるので,try catch文で囲んで使っていました.

もちろん例外を投げる方法も用意されていますが,この授業では扱いません.