連載
» 2004年01月08日 00時00分 UPDATE

いまから始めるJava(12):エラーに対処するプログラムを書く

[平井玄,@IT]

予期しないエラーに対処するには?

 プログラムのエラーには文法エラーのようにコンパイル時に発見できるものと、0で割り算してしまうような、コンパイル時には発見できないものがあります。あるいは、ハードディスクが故障してデータをファイルに書き込めないなど、実行時にはプログラマが想定できないようなエラーも発生します。

 このような想定外の場合に備えて、エラー発生時の処理を決めておくのが「例外」という機能です。まずは簡単な例を示しますので、どのような動作になるかを確認してみましょう。

10÷Nを計算する その1
import java.io.*;

public class DivideCalc {
  public static void main( String args[] ) {
    BufferedReader r = new BufferedReader( new InputStreamReader(System.in) );

    System.out.println( "10÷Nの計算をします" );
    System.out.println( "Nの値を入力してください:" );

    String s = r.readLine();
    int n = Integer.parseInt(s);

    System.out.println( "答えは" + (10 / n) + "です。" );
  }
}

 この実行結果は以下のとおりです。

C:\>javac DivideCalc.java
DivideCalc.java:10: 例外 java.io.IOException は報告されません。スローするにはキャッチまたは、スロー宣言をしなければなりません。
      s = r.readLine();
                     ^
エラー 1 個

C:\>

 一見正しくコンパイルできそうなプログラムでしたがエラーになってしまいました。なぜこのプログラムがコンパイルできなかったのか考えてみましょう。

 本連載で初めて登場するクラス「BufferedReader」はバッファからデータを取り出すために使うクラスです。コンストラクタの引数でnew InputStreamReader(System.in)と指定することにより、読み込み元のバッファを「InputStreamReader」からと指定しています。InputStreamReaderはバイト単位のデータを文字として読み込むためのクラスです。この場合、バイト単位のデータの読み込み元を「System.in」に指定しています。これまでのサンプルプログラムで何度となく登場していた「System.out」が画面出力を表すのに対して、System.inはキーボードからの入力を表します。従って「BufferedReader r = new BufferedReader( new InputStreamReader(System.in) )」全体では「キーボードから入力したバイト単位のデータを文字として読み込んでバッファにためるためのオブジェクトを生成し、そのオブジェクトにBufferedReader型の変数rでアクセスできるようにする」という意味になります。

 コンパイラがエラーを検出したのは「s = r.readLine();」の部分です。BufferedReaderクラスのメソッドであるreadLine()を実行するときに、「IOException」という例外が発生する可能性があるのに対処されていませんよ、というエラーです。readLine()のように、メソッドによっては発生し得る特定の例外についてプログラマが対処方法を決めておかなければならないのです。このとき使うのが「try〜catch」のブロックです。

10÷Nを計算する その2
public class DivideCalc2 {
  public static void main( String args[] ) {
    BufferedReader r = new BufferedReader( new InputStreamReader(System.in) );

    try {
      System.out.println( "10÷Nの計算をします" );
      System.out.println( "Nの値を入力してください:" );

      String s = r.readLine();
      int n = Integer.parseInt(s);

      System.out.println( "答えは" + (10 / n) + "です。" );
    } catch ( IOException e) {

      System.out.println( "キーボードが故障しているのかもしれません" );
    }
  }
}

 今回は無事にコンパイルできたはずです。「String s = r.readLine();」の部分でキーボードから文字を入力し、リターンキーを押したところで文字列が変数sに格納されます。さらに、変数sに格納された文字列はInteger型の静的メソッドであるparseInt()によって数値に変換され、int型の変数nに格納される、という流れです。parseInt()はHTMLDocument型クラスでHTMLを解釈するときに作ったparse()と同じように、文字列を解釈するメソッドです。

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

C:\>javac DivideCalc2.java
C:\>java DivideCalc2
10÷Nの計算をします
Nの値を入力してください:
5
答えは2です。

 変更後のプログラムでは、処理の大部分を「try { 〜 }」のブロックに入れました。さらに、「catch { 〜 }」ブロックで、tryブロック内で発生したIOExceptionの例外を捕捉し、エラーメッセージを表示するようにしました。try〜catchの構文では、tryブロックに本来の処理、catchブロックに例外発生時の処理を書くことで、予期しないエラーに対処できるようになります。

 ただ、このプログラムは発生し得る例外に対処し切れていません。例えば、キーボードから数字ではない文字を入力したらどうなるでしょうか? 試しに「abc」という文字列を入力してみましょう。

C:\>java DivideCalc2
10÷Nの計算をします
Nの値を入力してください:
abc
Exception in thread "main" java.lang.NumberFormatException:
 For input string: "abc"
        at java.lang.NumberFormatException.forInputString(NumberFormatException.
java:48)
        at java.lang.Integer.parseInt(Integer.java:468)
        at java.lang.Integer.parseInt(Integer.java:518)
        at DivideCalc2.main(DivideCalc2.java:12)

C:\>

 数値に変換できるはずのない「abc」という文字を入力したため、「int n = Integer.parseInt(s);」の部分で例外が発生してしまいました。実は、tryブロック内の処理で発生した例外を正しく捕捉するには、適切なcatchブロックを個々に記述しなければならないのです。

 ここでJava言語のAPI 仕様でIntegerクラスのparseInt()メソッドで発生する例外を調べると、文字列が構文解析可能な整数型を含まない場合に「NumberFormatException」という例外が発生することが分かります。そこで、プログラムを以下のように修正します。

10÷Nを計算する その3
public class DivideCalc2 {
  public static void main( String args[] ) {
    BufferedReader r = new BufferedReader( new InputStreamReader(System.in) );

    try {
      System.out.println( "10÷Nの計算をします" );
      System.out.println( "Nの値を入力してください:" );

      String s = r.readLine();
      int n = Integer.parseInt(s);

      System.out.println( "答えは" + (10 / n) + "です。" );
    } catch ( IOException e) {
      System.out.println( "キーボードが故障しているのかもしれません" );
    } catch ( NumberFormatException e ) {
      System.out.println( "数値に変換できる文字を入力してください" );
  }

}
}

 このプログラムを実行し、Nの値に「abc」を入力してみましょう。

C:\>java DivideCalc3
10÷Nの計算をします
Nの値を入力してください:
abc
数値に変換できる文字を入力してください

C:\>

 今回はエラーが表示されずにプログラム内で例外が捕捉されて、ユーザーには対処方法のメッセージが表示されました。「catch ( <捕捉したい例外のクラス> <変数名> )」と記述することで、例外ごとに対処方法を切り分けられるのです。

 一方、例外を捕捉できさえすればよい、という場合もあるでしょう。発生し得るすべての例外をいちいち調べていたらきりがありません。そこで、例外の種類を特定しない以下のようなcatchブロックの書き方もできます。

10÷Nを計算する その4
public class DivideCalc4 {
  public static void main( String args[] ) {
    BufferedReader r = new BufferedReader( new InputStreamReader(System.in) );

    try {
      System.out.println( "10÷Nの計算をします" );
      System.out.println( "Nの値を入力してください:" );

      String s = r.readLine();
      int n = Integer.parseInt(s);

      System.out.println( "答えは" + (10 / n) + "です。" );
    } catch ( Exception e ) {
      System.out.println( "何かの例外が発生したので処理を続行できませんでした" );
    }

  }
}

例外発生の有無に関係なく実行するための
−finallyブロック−

 catchブロックは例外を捕捉し、例外発生時の処理を記述するために使いますが、例外が発生してもしなくても処理しなければならないことはどうしたらいいのでしょうか?

 例えば、テキストファイルの内容を画面に表示するプログラムを考えてみます。WindowsなどのOSが管理しているファイルを読み込むには、まず特定のファイルを扱うことをOSに対して宣言(オープン)し、その後、使い終わったことをOSに通知(クローズ)しなければなりません。ディスクの読み込み中にエラーが発生し例外を捕捉しても、そのままプログラムを終了させてしまうのではなく、必ずファイルを閉じなければならないのです。

 このように、特定のリソースの利用開始から終了するまでに起こる例外を捕捉しつつ、最後に確実にリソースを解放することは、ユーザーに安定したサービスを提供するプログラムを書くために欠かせないことです。そこでfinallyという文を使います。

テキストファイルを表示する
import java.io.*;

public class ShowTextFile {
  public static void main( String args[] ) {
    if ( args.length == 0 ) return;
    try {
      FileInputStream fis = new FileInputStream(args[0]);
      try {
        int i;
        for ( i = fis.read(); i != -1; i = fis.read() )
          System.out.print((char)i);
        System.out.println();
      } catch ( IOException e ) {
        System.out.println("ディスクI/Oエラー");
        return;
      } finally {
        fis.close();
      }

    } catch ( FileNotFoundException e ) {
      System.out.println("ファイルが見つかりません");
      return;
    } catch ( IOException e ) {
      System.out.println("ファイルを閉じられません");
      return;
    }
  }
}


 tryブロックの中にtryブロックがある、一見複雑な構造になりました。そこで重要な部分を抜き出して説明します。

FileInputStream fis = new FileInputStream(args[0]);
try {
  int i;
  for ( i = fis.read(); i != -1; i = fis.read() )
    System.out.print((char)i);
  System.out.println();
  } catch ( IOException e ) {
    System.out.println("ディスクI/Oエラー");
    return;
  } finally {
    fis.close();
}

 1行目では、テキストファイル読み込み用のクラスFileInputStreamのオブジェクトを生成しています。FileInputStreamクラスのコンストラクタの引数になっている「args[0]」は、コマンドラインでこのプログラムを実行したときの1番目の引数を格納する配列です。例えば、「java ShowTextFile TEST.TXT」と入力したときのargs[0]には「TEST.TXT」が格納されます。

C:\>javac ShowTextFile.java
C:\>java ShowTextFile test.txt

(ここにtest.txtの内容が表示される)

C:\>


制御文としての役割

 例として挙げたテキストファイルを読み込んで表示するプログラムの主要部分は、3つの部分に分かれています。tryブロックでは、データを1文字ずつ読み込んで、ファイルの終わりに達するまで画面に表示します。catchブロックではIOExceptionの例外を捕捉し、ディスクI/Oエラーによる読み込みの中断に備えています。最後のfinallyブロックでは、現在開いているファイルを閉じています。

 この例で重要なのは、最後のfinallyブロックはたとえ例外が発生しても必ず実行されることです。例えば、ディスクI/Oエラーが発生してcatchブロックに制御が移ったとします。このときプログラムが画面に「ディスクI/Oエラー」と表示することまでは通常の文と同じです。しかし、次にあるreturn文が実行されてメソッドを終了するとき、すぐにはメソッドを抜け出さずにfinallyブロックにある「fis.close()」が実行されるのです。「第6回 プログラムの制御構造を理解する」でreturnを「メソッドを呼び出した親メソッドに制御を戻す」文と説明しました。しかし、try〜catch〜finallyの場合は実行順が通常とは少し異なり、finallyブロックにある文を実行してから親メソッドに制御が戻されます。つまり、例外処理はreturnやbreakといった処理を中断させる制御文のふるまいを若干変える効果があるのです。

 また、この例ではファイルをオープンして内容を表示するのに関連する処理が、本来の処理、エラー時の処理、終了処理の3つに区分けされました。例外を使うと、forやwhileなどの制御文とは別の意味で、プログラムの構造を役割に応じて記述できるのです。そこで次回は、プログラムを分かりやすく記述するという例外のもう1つの側面について説明します。


Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。