技術メモ

技術メモ

ラフなメモ

Javaの基本事項を復習(例外処理)

6 章: 例外処理

例外の基本

例外には大きく 3 つの種類があります。

  • 検査例外(Exception)
  • 実行時例外(Runtime Exception)
  • エラー(Error)

検査例外(Exception)

プログラム作成時に想定できる異常を通知するために使用します。検査例外を使用すると想定される異常に対応する処理が存在することをコンパイル時にチェックできるようになります。

実行時例外(Runtime Exception)

プログラム作成時に想定されないエラーを通知するために使用します。バグや設定ミスなどによることが多い。プログラムで捕捉せずとも呼び出し元で発生します。

エラー(Error)

例外とは異なります。システムの動作を継続できない 致命的なエラー、を示します。

例外を表すクラス

f:id:tutuz:20190323112638p:plain

1.java.lang.Exception クラス

検査例外を表すクラス。以下は継承していくクラスの例です。

f:id:tutuz:20190323113644p:plain

このクラスを継承した例外(RuntimeException を除く) はプログラム中で捕捉されるか、発生するメソッドで throws を記述する必要がある。throws 節を用いると、その処理で例外が発生することがわかります。宣言で例外が記述されているメソッドは呼び出し元でその例外に対して何らかの処理をすることが Java の言語仕様として要求されます。

Sample1_1.java

   public static void main(String[] args) {
        try {
            readFile();
        } catch (IOException e) {
            // 例外が発生したときに何かをする
            e.printStackTrace();
        }
    }

    public static List<String> readFile() throws IOException {
        // 何らかの処理
        return null;
    }

2.java.lang.RuntimeException クラス

実行時例外を表すクラスです。このクラスを継承した例外(例えば java.lang.ClassCastException, java.io.FileNotFoundException, java.util.ConcurrentModificationException などなど) はプログラム中で必ずしも捕捉する必要はありません。またメソッドの宣言で throws を記述する必要もない。呼び出し側で catch も throws も記述しなかった場合は、発生した実行時例外は呼び出し元に自動的に伝播します。

どこでも捕捉されなかった場合は「そのスレッドが終了する」ことになります。JVM がスレッドを生成していますが、JVM に例外が到達して時点でそのスレッドは終了します。

3.java.lang.Error クラス

Error は Throwable のサブクラスで、通常のアプリケーションであればキャッチすべきではない重大な問題を示します。そうしたエラーの大部分は異常な状態です。

例えば java.lang.OutOfMemoryError は有名な Error です。これは java.lang.VirtualMachineError のサブクラスです。このエラーが発生した場合はアプリケーションを速やかに終了スべき状態であることを意味しています。

例外処理

try ... catch ... finally

Sample1_2.java

   public static void main(String[] args) {

        byte[] contents = new byte[100];
        InputStream is = null;
        try {
            is = Files.newInputStream(Paths.get("/"));
            is.read(contents);
        } catch (IOException e) {
            // 例外発生時の処理
            e.printStackTrace();
        } finally {
            // try - catch ブロック終了時に必ず実行する処理
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    // nothing
                }
            }
        }
    }

finally はストリームやデータベース接続のような、使用後に必ず解放しなければならないリソースのオブジェクトを用いる場合によく使われます。 finally でリソースを解放しないと、例外が発生した場合にリソースが解放されないことになってしまいます。

try ... with ... resources

Sample1_2.javaJava 7 からは以下のように書ける。

Sample1_3.java

   public static void main(String[] args) {

        byte[] contents = new byte[100];

        try (InputStream is = Files.newInputStream(Paths.get(""))) {
            is.read(contents);
        } catch (IOException e) {
            // 例外発生時の処理
            e.printStackTrace();
        }
    }

InputStream などのリソースを扱うクラスは AutoCloseable, Closeable を実装するようになっています。 try ブロックの開始時に AutoClosable インターフェースの実装クラスを宣言しておくと、その try - catch ブロック終了時に行う close メソッドを自動的に呼び出すようになっているためです。

ただし try の開始時の宣言で処理を長々記述するとプログラムの可読性が落ちる可能性があります。リソース確保に関係のない処理は記述するべきではない、ということのようです。

マルチキャッチ

省略

例外処理のポイント

エラーコードを return しない

例外機構のない言語(シェルとか)エラーコードを return することが多いですが、Java では例外機構が備わっているのでエラーコードを return するのではなく、例外を発生させるべきです。

正常に処理ができれば、そのオブジェクトを戻り値で返せばよく、呼び出し元から見るとオブジェクトが返って来たので処理自体は成功したと考えることができます。

例外をもみ消さない

以下のようにすると例外が発生したとしても、何も表示されないので例外が発生したかどうかもわからなくなります。

   public static void main(String[] args) {

        byte[] contents = new byte[100];

        try (InputStream is = Files.newInputStream(Paths.get(""))) {
            is.read(contents);
        } catch (IOException e) {
            // 例外発生時の処理
            // 何もしない
        }
    }

本当に何もすることがない場合は、以下の 2 つの方法があるようです。

ログの出力

ログを出力すれば、例外発生時にトラックすることができます。ログに出力する場合は、例外のスタックトレースもログに出力させるようにします。

Sample1_4.java

   public static void main(String[] args) {

        Logger log = LoggerFactory.getLogger(Sample1_4.class);

        String str = "abc";
        try {
            int num = Integer.parseInt(str);
            System.out.printf("num = %d", num);
        } catch (NumberFormatException e) {
            log.warn("Not Integer.", e);
        }
    }

処理継続の判断

例外が発生したときに、後続処理を継続しても NullPointerException が発生するなど意味のない処理になることがあります。その場合は例外が発生した場合はただしに throws するか、デフォルト値を与えて後続処理を継続するか。という対応が考えられます。

throws Exception しない

複数の例外が発生するから... といって throws Exception を宣言してしまうのはアンチパターンです。

まず呼び出し元で Exception を補足しなければなりませんが、その理由がわかりくくなります。また具象クラス (IOException) などの例外が発生しても Exception で捕捉されてしまうため、例外の内容によって処理を分けることができなくなってしまいます。さらに RuntimeException が発生しても Exception に巻き込まれることになります。

これは重大な問題で、以下のようなことが考えられます。

  • RuntimeException は実行時例外なので本来は補足する必要はないが、補足しなければならなくなる
  • 実行時例外が発生した場合に正しくない処理を行ってしまうことがある
  • 呼び出し元で捕捉して上位に伝播させなければ、本当は処理が必要な実行時例外を見逃してしまう可能性がある

Sample1_5.java

   public static void main(String[] args) {

        try {
            caller();
        } catch (Exception e) {
            // something
        }

    }

    static public void caller() throws Exception {
        Class someClass = Class.forName("Sample1_5");
    }

以下の例では、実行時例外である NullPointerException を捕捉してしまっているので、プログラムのバグに気づくことができません。

Sample1_6.java

   public static void main(String[] args) {

        try {
            caller();
        } catch (Exception e) {
            // something.
        }

    }

    static void caller() throws Exception {
        String str = null;
        System.out.printf("str.length = %d", str.length());
    }

どの階層で例外を捕捉して処理するか

例外を発生される処理は末端の処理になります。ここではあくまで例外を発生させるだけにしておきます。処理の流れを判断するタイミングで例外を捕捉することで全体の流れをわかりやすくできるのと、変更時の保守性がよくなります。

独自例外の作成

以下の条件を満たす場合は独自例外を作るべきといえます。

(具体的な実装は後ほど書く)

例外のトレンド

検査例外よりも実行時例外を使う

ラムダ式の中で発生した例外の扱い

Optional クラス導入のメリット