不都合が生じたら(例外) 戻る
下位のメソッドで値をチェックする

オーバーロードを用いた以下のサンプルを参照下さい。年齢が0未満になることはありませんので、年齢が負値の場合はエラーを表示することにします。実際のチェックはcreateMessage(String, int)の中でチェックを行いたいと思います。チェックの結果、年齢が負値の場合、戻り値でnullを返すことにしましょう。

class Sample { static final String MESSAGE_1ST = "私は"; static final String MESSAGE_2ND = "です。"; static final String MESSAGE_3RD = "歳" + MESSAGE_2ND; public static void main(String[] args) { Sample instance = new Sample(); String message = instance.createMessage(args); System.out.println(message); } String createMessage(String[] args) { return createMessage(args[0], args[1]); } String createMessage(String name, String age) { int ageInt = Integer.parseInt(age); String message = createMessage(name, ageInt); return message; } String createMessage(String name, int age) { if (age < 0) { return null; } return age + MESSAGE_3RD + MESSAGE_1ST + name + MESSAGE_2ND; } }

createMessage(String, String)の中でcreateMessage(String, int)を呼び出している箇所も「nullが返ってきたら」という判定が必要になる場合があります。

String createMessage(String name, String age) { int ageInt = Integer.parseInt(age); String message = createMessage(name, ageInt); if (message == null) { return null; } return result; }

今回のプログラムではnullが返ってきた時にnullを上位に返しているのでこのような対応を加えなくても大丈夫なのですが、上に上に不都合が発生した原因をしらせなければならない場合はあります。

プログラムを1行ずつ検証する

プログラムを起動してから年齢のチェック、エラーを表示する、までの動作を1行ずつ検証します。

class Sample { static final String MESSAGE_1ST = "私は"; static final String MESSAGE_2ND = "です。"; static final String MESSAGE_3RD = "歳" + MESSAGE_2ND; public static void main(String[] args) { Sample instance = new Sample(); String message = instance.createMessage(args); if (message == null) { System.out.println("エラーが発生しました。"); } else { System.out.println(message); } } String createMessage(String[] args) { return createMessage(args[0], args[1]); } String createMessage(String name, String age) { int ageInt = Integer.parseInt(age); String message = createMessage(name, ageInt); if (message == null) { return null; } return message; } String createMessage(String name, int age) { if (age < 0) { return null; } return age + MESSAGE_3RD + MESSAGE_1ST + name + MESSAGE_2ND; } }
  1. Javaが09行目のmainを呼び出す
  2. 11行目でcreateMessage(String[])を呼び出す
  3. 19行目に移動
  4. 20行目でcreateMessage(String, String)を呼び出す
  5. 23行目に移動
  6. 25行目でcreateMessage(String, int)を呼び出す
  7. 32行目に移動
  8. 33行目のifがtrueのため34行目を実行
  9. 34行目でnullを呼び出したプログラムに返す
  10. 25行目に戻る
  11. 26行目のifがtrueのため27行目を実行
  12. 27行目でnullを呼び出したプログラムに返す
  13. 20行目に戻る
  14. 11行目に戻る
  15. 12行目のifがtrueのため13行目を実行
  16. 13行目でエラーを表示

本来であれば34行目の時点で次に実行したいのは13行目になります。ですがプログラム言語は呼び出し元を順にさかのぼらないと戻れないので、余計な処理をしながら1つずつ呼び出し元に戻っていきます。

現在のプログラムの実行位置をボールに例えるなら、34行目の時点でボールを呼び出し元に投げます。この際、呼び出してきた履歴を無視して一気に上位にボールを投げます。このボールのことをJava言語では例外と呼びます。

想定外のことが発生したら例外を投げると覚えましょう。

例外を投げる

では実際に例外を投げてみます。プログラムを以下のように変更します。記述順は「throw 投げたい例外のインスタンス;」になります。例外はとても強力なので壁でも何でもぶち破って上位に投げられると覚えて下さい。よってこの例外はmainを呼び出したJavaまで届きます。

String createMessage(String name, int age) { if (age < 0) { RuntimeException e = new RuntimeException(); // インスタンスを生成する。 throw e; // インスタンスを投げる。 } return "私は" + name + "です。" + age + "歳です。"; }
例外を投げる

上記のようにプログラムを変更し、実際に例外を投げなさい。その際、何が起こるか予想、実行、予想と実測が一致したか検証しなさい。

例外を捕る

先ほどの変更では13行目を実行することなくmainを呼び出したJavaまで届いてしまいました。壁をぶち破りすぎです。今度は例外を捕まえる網を張ってみます。具体的には以下のようにプログラムを変更します。

public static void main(String[] args) { Sample instance = new Sample(); try { String message = instance.createMessage(args); System.out.println(message); } catch (RuntimeException e) { System.out.println("エラーが発生しました。"); } }

この記述は「try { ... } の中で投げられてきた例外のうち、catch () のカッコ内にかかれた例外が投げられたら、その例外を捕まえる。捕まえた例外をe(今回例がeであって名称は自由です)という変数にメモしておく。」という意味になります。この想定外の事象を例外で処理すると以下のようなプログラムになります。この章の最初のプログラムに近く、随分簡潔になりました。

class Sample { static final String MESSAGE_1ST = "私は"; static final String MESSAGE_2ND = "です。"; static final String MESSAGE_3RD = "歳" + MESSAGE_2ND; public static void main(String[] args) { Sample instance = new Sample(); try { String message = instance.createMessage(args); System.out.println(message); } catch (RuntimeException e) { System.out.println("エラーが発生しました。"); } } String createMessage(String[] args) { return createMessage(args[0], args[1]); } String createMessage(String name, String age) { int ageInt = Integer.parseInt(age); String message = createMessage(name, ageInt); return message; } String createMessage(String name, int age) { if (age < 0) { throw new RuntimeException(); // インスタンスの生成と同時に投げてみました。 } return age + MESSAGE_3RD + MESSAGE_1ST + name + MESSAGE_2ND; } }

このように想定外の事象が発生した場合、例外で処理すると覚えておきましょう。

例外と実行時例外

Java言語では例外は大きく分けて2つ存在します。 いままで利用してきた「文字列を整数にする」というものは実際に値が渡されるまで例外が発生するかわかりません。 対してファイルを扱うプログラムを作成したとすると「ファイルを扱うからファイルが存在しないのはよくある話」 という2つに、Java言語ではわけて考えます(筆者にも上記の2つの例外の線引がどこにあるかは、わかりません)。 Java言語では前者を実行時例外、後者を例外と呼びます。

ここでJavaそのもののソースを見てみます。 登場するクラス名は3つ。英語から判断するなら例外はException、実行時例外はRuntimeException、Throwableは何でしょう。 Java言語ではよくみかけるのですが動詞にableをつけた「何々することができる」を表すのにableを利用します。つまり投げることができるクラスというところでしょうか。 この3つのクラスから判断すると「投げることができるクラス」、それを継承した「例外クラス」、更に例外クラスでも実行時の専用が「実行時例外クラス」という親子関係が成り立ちます。

public class Exception extends Throwable // 例外のクラス宣言

public class RuntimeException extends Exception // 実行時例外のクラス宣言
例外を投げる可能性(throws句)

先ほどの例外を投げているメソッドを見てみます。このメソッドでは条件によっては例外を投げる可能性があります。このような場合、Java言語では呼び出したプログラムがどのような例外を捕まえればよいか伝える方法があります。その方法をJava言語では「throws句」と呼びます。

記述する方法ですがメソッドを作成する際に末尾に記述します。記述順ですが「戻り値 メソッド名(あればパラメータ) throws 投げる可能性のある例外」になります。複数の例外を投げる可能性がある場合はthrows句にカンマつなぎで複数の例外を記述します。以下が記述例になります。

public static void main(String[] args) throws Exception, RuntimeException {

さてここで混乱しやすいキーワードなのですがthrowとthrowsとを誤って記述する方がいらっしゃいます。覚え方ですが「例外を投げるのは1つ、投げる可能性があるのは複数」なのでメソッド宣言に記述するのは「throws」になります。

今まで登場したメソッドではthrows句は登場しませんでした。ではthrows句はメソッドを呼び出した方への親切心だけなのでしょうか。実は「実行時例外のサブクラスを投げる際はthrows句に投げる可能性のあるクラスを書かなくてもよい」というルールがあるのみで、実行時例外は除く例外とそのサブクラスは記述しないといけないのです。今までたまたま例外のサブクラスを投げていなかったのでthrows句の記述がなかったのです。

例外を投げる可能性をつたえない

実行時例外を投げる可能性がある場合はthrows句に省略して良いというルールがありますが、例外の場合は省略できません。実際にどのようなことが起こるか試してみなさい。試す前に何が起こるか予想してから試しなさい。

finally

Java言語では例外が発生するとそこで処理が中断しcatchされるまで例外が飛んでいってしまいます。

筆者は幼少期に「出したらしまう!」「開けたら閉める!」とよく怒られたものです。Java言語では例外が投げられてしまったけど、もしくは、例外が投げられてはないけど、双方で同じ事を実行できる仕組みがあります。

プログラム(のようなもの)で見てみます。この例だと「中のものを出す」時点で「なかった例外」が投げられるでしょう。ですがあってもなくても「引き出しを閉める」は実行したいのです。

public static void main(String[] args) { try { 引き出しを開ける。 中のものを出す。 出したものを逆さまにする。 逆さまを戻す。 中にしまう。 引き出しを閉める。 } catch (ものがなかった例外) { 引き出しを閉める。 } }

このような場合にfinallyを利用します。finallyはどちらの場合も実行しますので「引き出しを閉める」は1箇所のみの記述になります。

public static void main(String[] args) { try { 引き出しを開ける。 中のものを出す。 出したものを逆さまにする。 逆さまを戻す。 中にしまう。 } catch (ものがなかった例外) { } finally { 引き出しを閉める。 } }

すると今度はcatchの中身がなくなってしまいました。このような場合catchを省略することができます。

public static void main(String[] args) { try { 引き出しを開ける。 中のものを出す。 出したものを逆さまにする。 逆さまを戻す。 中にしまう。 } finally { 引き出しを閉める。 } }

このようにtryの場合はcatchとfinallyが存在したり存在しなかったりいろいろありますので、必要に応じ使い分けましょう。

catchとfinally

実際に自分でcatchとfinallyのサンプルを作成し実行しなさい。実際にどのようなことが起こるか試してみなさい。

よくある例外

初心者がよく出会う例外を紹介します。

  • NullPointerException

    変数の内容がnullの状態で、その変数の関数を呼び出すと発生します。

    クラス階層 java.lang.Object  java.lang.Throwable   java.lang.Exception    java.lang.RuntimeException     java.lang.NullPointerException
    NullPointerExceptionが発生するサンプルソース class NullPointerSample { public static void main(String[] args) { Integer num1 = new Integer(1); String str1 = num1.toString(); System.out.println(str1); Integer num2 = null; String str2 = num2.toString(); // ココで例外 System.out.println(str2); } }
  • ClassCastException

    クラスをキャスト変換する際、実際にキャスト変換ができない場合に発生します。

    クラス階層 java.lang.Object  java.lang.Throwable   java.lang.Exception    java.lang.RuntimeException     java.lang.ClassCastException
    ClassCastExceptionが発生するサンプルソース class ClassCastSample { public static void main(String[] args) { Object instance = new Integer(1); String str = (String) instance; // ココで例外 } }
  • ArrayIndexOutOfBoundsException

    配列で用意されている個数より大きい場所が指定されると発生します。

    クラス階層 java.lang.Object  java.lang.Throwable   java.lang.Exception    java.lang.RuntimeException     java.lang.ArrayIndexOutOfBoundsException
    ArrayIndexOutOfBoundsExceptionが発生するサンプルソース class ArrayIndexOutOfBoundsSample { public static void main(String[] args) { int[] num = new int[] {1, 3, 5}; System.out.println(num.length); System.out.println(num[0]); System.out.println(num[1]); System.out.println(num[2]); System.out.println(num[3]); // ココで例外 } }
  • IOException

    入出力操作(ファイルの読み込み、書き込み等)で予想外のことが発生した場合に発生します。

    ここまでの勉強でファイル操作系の勉強はしていませんがExceptionのサブクラスの1例として記述しました。

    クラス階層 java.lang.Object  java.lang.Throwable   java.lang.Exception    java.io.IOException
    IOExceptionが発生するサンプルソース import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class IOExceptionSample { public static void main(String[] args) throws IOException { File file = new File("c:/abc.txt"); // c:\abc.txt が存在しないファイルとします。 InputStream is = null; try { is = new FileInputStream(file); // ココで例外 } finally { if (is != null) { is.close(); } } } }
例外をthrow

実際に自分で上記のサンプルを実行しなさい。例外発生の箇所が正しいか確認しなさい。