14 例外に対処する

【第14回】例外に対処する

今回は、例外処理についての紹介です。
複数クラスを使用した例題がありますので、今回の前提条件は以下の通りです。
(???という人は「第10回 複数のクラスを扱う」を読み直してみて下さい)

前提: 複数クラスを用いたプログラムを作成(打ち込んだ)した事がある。

「レイガイ」ってなんでしょう? 言葉の意味はわかりますが、
Javaプログラミングとどうつながるか、いまいちピンときませんね。
まず、プログラミングにおける例外処理とは何か、なぜそれが必要かについて説明しましょう。

これまでの講座で、何度も学んできた通り、コンピュータの仕事の多くは、
「入力」、「処理」、「出力」で構成されています。
例えばコンピュータに65536÷4096という割り算をさせる
としましょう。この場合、

入力:65536と4096(計算に必要な値)
処理:割り算
出力:16(計算結果)

となります。
使う側にとっては、入力を変化させることで、あっという間に出力を得られるところが
コンピュータの便利なところですよね。

さて、処理を作る側に立っている私たちは、どんな要求にも応えられるようにしなければなりません。例えば・・・

123÷0 (0では割れません)
456÷abc (数値ではなくて文字列・・・)

こんなとき、あなたならどうしますか? ここからは、実際にプログラムを書きながら考えてみましょう。

まず、先ほどの例をJavaプログラムで書いてみます。
もうおわかりかと思いますが、「入力」は引数、「処理」はメソッド、
「出力」はメソッドの戻り値に相当します。そこで、divideというメソッドを作成してみましょう。
引数として x と y をとって、割り算した結果を返すメソッドです。

簡単ですね。話を簡単にするために、ここでは引数を整数(int型)戻り値もint 型としておきます。
divideメソッドを使って、コマンドラインから引数で与えられた2つの数を使い、割り算を行って、
結果を表示するプログラム、DivisionProcessを作成します(リスト1)。
せっかく複数のクラスについて学んでいますから、mainメソッドを含むクラスは
DivisionTestとしましょう(リスト2)。

mainメソッドの引数、args は今回初めて使用しますが、この説明は話の本筋からそれますので、
ここでは「そういうものだ」程度に考えておいてください。

リスト1 – DivisionProcess.java

public class DivisionProcess
{
    // x を y で割った結果を返すメソッド
    public int divide(int x, int y)
    {
        int answer = x / y;
        return answer;
    }
}

リスト2 – DivisionTest.java

public class DivisionTest
{
    public static void main(String[] args)
    {
        int x;      // 割られる数
        int y;      // 割る数
        int answer; // 答え

        // 引数を数値に変換する
        x = Integer.parseInt(args[0]);
        y = Integer.parseInt(args[1]);
        
        // 割り算を実行する
        DivisionProcess divisionProcess = new DivisionProcess();
        answer = divisionProcess.divide(x, y);

        // 結果を表示する
        System.out.println(x + " / " + y + " = " + answer);
    }
}

さっそくこのプログラムをコンパイルして実行してみましょう。
先ほどの例のように、65536÷4096を計算させてみます。
コマンドラインから次のように実行してみてください。

C:\work>java DivisionTest 65536 4096
65536 / 4096 = 16

C:\work>

正しく答えが表示されましたね?
ここで、意地悪をしてみましょう。123÷0 を実行してみてください。

C:\work>java DivisionTest 123 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
        at DivisionProcess.divide(DivisionProcess.java:6)
        at DivisionTest.main(DivisionTest.java:15)

C:\work>

なにやら、訳の分からないメッセージが表示されましたね?
さらに意地悪をして、456÷abc を実行してみましょう。

C:\work>java DivisionTest 456 abc
Exception in thread "main" java.lang.NumberFormatException: abc
        at java.lang.Integer.parseInt(Unknown Source)
        at java.lang.Integer.parseInt(Unknown Source)
        at DivisionTest.main(DivisionTest.java:11)

C:\work>

今度も先ほどと似ていますが、別のメッセージが表示されました。
こんなメッセージを出すようにプログラムした覚えはありませんよね。

ここで表示されるこれらのメッセージは、Java によって表示される「例外」なのです。
わかりにくければ、まずは「エラー」と考えてもらっても構いません。

123÷0 を実行したときの例外メッセージには、
「java.lang.ArithmeticException: / by zero」と表示されています。
divide メソッドで割り算を実行しようとしたときに、割る数である y に 0 が入っていたために
「ゼロでは割れん!!(ArithmeticException)」と Javaが怒っているわけです。

また、456÷abc を実行したときの例外メッセージには、
「java.lang.NumberFormatException: abc」と表示されています。
これは、main メソッド内の次の行で文字列を数値に変換しようとしたところで発生したものです。

        y = Integer.parseInt(args[1]);

「456」は数値に変換できますが、「abc」は数値に変換できませんね。
そこで、「数値の書式がおかしい!!(NumberFormatException)」とJavaが怒っているのです。

このように、プログラムが外部(この場合は引数ですね)から情報を受け取って
様々な処理を行う場合、与えられる情報が必ずしもこちらの意図したとおりであるとは限りません。

意図していないとはいえ、こんな訳の分からないメッセージが表示されるのは格好悪いですよね。Javaに怒られないようにするためには、どうすればよいのでしょうか?

ユーザからの入力をチェックするというのが簡単な方法です。
割る数がゼロだった場合にはもっとわかりやすいエラーを表示するように、
プログラムを直してみましょう(リスト3)。

リスト3 – DivisionTest.java

public class DivisionTest
{
    public static void main(String[] args)
    {
        int x;      // 割られる数
        int y;      // 割る数
        int answer; // 答え

        // 引数を数値に変換する
        x = Integer.parseInt(args[0]);
        y = Integer.parseInt(args[1]);

        if(y == 0)
        {
            System.out.println("エラー:0では割れません!!");
            return;
        }
        
        // 割り算を実行する
        DivisionProcess divisionProcess = new DivisionProcess();
        answer = divisionProcess.divide(x, y);

        // 結果を表示する
        System.out.println(x + " / " + y + " = " + answer);
    }
}

リスト3をコンパイルして、先ほどと同じように 123÷0 を 実行してみてください。
今度はこちらで用意したメッセージが表示されましたね。

C:\work>java DivisionTest 123 0
エラー:0では割れません!!

C:\work>

でも、456÷abcのように文字列を指定されたり、さらには引数を一つしか
与えてもらえなかったときなどはどうすればよいでしょう?

そのたびに判定文を加えていかなければいけませんね。

このように、様々な状況にきちんと対処できるプログラムを書くのは大変な作業です。
個人的なプログラムを作るなら多少のことには目をつぶれますが(使うのは自分だけですからね)、
多くの人々に使ってもらう製品ともなるとそうはいきません。

とはいえ、意図しないあらゆる事態(「異常系」といいます)に対処できるようにプログラムを書くと、
本当に行いたい処理が見えなくなってしまうのです。

今回の例でも、コンピュータに本当に行わせたい処理は divide メソッドの部分だけなのに、
先ほど追加したエラー処理の方が多くなってしまいましたよね。
二つならまだしも、こういった異常系は考え出すときりがありません。

さて、ここで登場するのが「例外処理」です(ずいぶん前置きが長くなってしまいましたが・・・)。
実は先ほどから表示されいてる ~Exception が例外の正体です。
(「Exception」とは 「例外」のことですね)

ゼロで割り算をしたときや、abc を数値に変換しようとしたときに、Javaは「例外」を生成します。
例外には様々な種類があり、その種類によって異常の原因を知ることができます。
この例外をプログラムの中で「捕まえ」て、それに応じた対処をすることができます。
プログラムの中で例外を「捕まえ」なかった場合、Javaはどう対処したらよいかわからないので、
リスト1の例のように例外メッセージを表示してプログラムを止めてしまうのです。

下の表は、Javaに用意されている例外のほんの一例です。例外には様々な種類がありますが、
よく出てくるものはわずかですので、必要に応じて覚えておけばよいでしょう。

例外内容
ArithmeticException ゼロで割り算しようとしたとき
NumberFormatException 文字列を数に変換できなかったとき
ArrayIndexOutOfBoundsException 配列の範囲外にアクセスしようとしたとき

まずはリスト3 を例外処理を使って書き換えてみましょう(リスト4)。
リスト4 – DivisionTest.java

public class DivisionTest
{
    public static void main(String[] args)
    {
        int x;      // 割られる数
        int y;      // 割る数
        int answer; // 答え

        try
        {
            // 引数を数値に変換する
            x = Integer.parseInt(args[0]);
            y = Integer.parseInt(args[1]);
            
            // 割り算を実行する
            DivisionProcess divisionProcess = new DivisionProcess();
            answer = divisionProcess.divide(x, y);
        }
        catch (NumberFormatException ex)
        {
            // 数値ではない文字列が指定された場合の処理
            System.out.println("エラー:数値ではありません!!");
            return;
        }
        catch (ArithmeticException ex)
        {
            // ゼロで割ろうとしたときの処理
            System.out.println("エラー:0では割れません!!");
            return;
        }
        catch (ArrayIndexOutOfBoundsException ex)
        {
            // 引数が足りなかったときの処理
            System.out.println("エラー:引数が足りません!!");
            return;
        }

        // 結果を表示する
        System.out.println(x + " / " + y + " = " + answer);
    }
}

このプログラムの中で、次の部分が例外処理のキモです。
try{} の中に例外が発生する可能性がある処理を記述し、
catch() {} の部分に発生した例外に対する処理
を記述します。

try
{

}
catch (…)
{

}

リスト3では、文字列を数値に変換する処理と、divideメソッドを使う処理が
try文の中に書かれています。
この部分では、先ほど試したようにいろいな場合に対処しなければいけませんでした。
実際に例外が発生したときにどうするかがを、例外の種類毎に catch 文の中に書かれています。
ここでは、

・数値ではない文字列が指定されたとき
・ゼロで割ろうとしたとき
・引数が足りなかったとき

の3つの例外について対処しています。それ以外にも対処しなければいけない例外があれば、
catch 文を増やして付け加えていけばよいのです。

こうすることで、処理の本筋と例外処理を明確に分けることができるのです。
この考え方は、Java言語全体で使われています。
わかりにくければ、try文 とcatch文の使い方は次のようなに捉えてください。

「まずやってみる(try)。だめだったら、そのときに考える(catch)。」

それでは、今回のまとめを。

~ まとめ ~

1.エラー処理はとにかく大変だ
2.例外を使うとエラー処理をスマートに書ける
3.例外処理は try ~ catch 文で行う

例外処理は非常に奧が深く、今回紹介したのは今後必要となるほんの一握りの部分で、
全てを説明しきることはできません。
さらに突っ込んだ例外の使い方については、また次の機会にしましょう。

→ 次へ(第15回:「ご注文はなんですか?」~入力ライブラリの利用~)