Java 逆引き集 | 例外処理(try / catch / finally) - 安定性と後始末

Java Java
スポンサーリンク

例外って何者か(「普通じゃない事態」のシグナル)

例外は「プログラムが通常の流れでは処理できない異常事態が起きた」というシグナルです。 ファイルが見つからない、ネットワークが切れた、数値に変換できない文字列が来た、など「想定外だけど現実にはよく起きること」を、Javaは例外として表現します。

例外処理は、そのシグナルを受け止めて「落ちずに済ませる」「後始末をきちんとする」ための仕組みです。 ここをちゃんと設計できるかどうかが、システムの安定性と信頼性に直結します。

try / catch の基本構造と「落ちないコード」

try で「危ない処理」を囲う

例外が起きる可能性がある処理を、try ブロックで囲みます。

try {
    int value = Integer.parseInt("123");
    System.out.println("変換結果: " + value);
} catch (NumberFormatException e) {
    System.out.println("数値に変換できませんでした");
}
Java

Integer.parseInt("123") は、文字列が数値として不正な場合に NumberFormatException を投げます。 try の中で例外が発生すると、そこで通常の処理は止まり、対応する catch にジャンプします。

「ここで何かトラブルが起きるかもしれない」と分かっている場所を、まず try で囲う。 それが例外処理のスタートラインです。

catch で「どう振る舞うか」を決める

catch は「この種類の例外が来たら、こう対応する」という場所です。

catch (NumberFormatException e) {
    System.out.println("数値に変換できませんでした");
}
Java

ここでやることは状況次第ですが、代表的なのは次のような振る舞いです。

エラーメッセージを出す ログに記録する ユーザーに再入力を促す 安全なデフォルト値に切り替える

重要なのは、「例外を握りつぶさない」「何が起きたか分かる形で残す」ことです。 catch の中を空にしてしまうと、「問題は起きているのに誰も気づかない」状態になり、後から原因調査が地獄になります。

複数の catch と例外の種類

例外クラスごとに分けて受け止める

Javaの例外はクラス階層になっていて、種類ごとに別のクラスになっています。 例えば、ファイル関連なら IOException、数値変換なら NumberFormatException、配列の範囲外なら ArrayIndexOutOfBoundsException などです。

1つの try に対して、複数の catch を並べることができます。

try {
    String text = "123";
    int value = Integer.parseInt(text);
    int[] array = new int[2];
    System.out.println(array[5]);
} catch (NumberFormatException e) {
    System.out.println("数値変換エラー: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("配列の範囲外アクセス: " + e.getMessage());
}
Java

どの例外が投げられたかによって、対応を変えられるのがポイントです。 ビジネスロジックでは、「このエラーならユーザーにこう伝える」「このエラーならシステム管理者向けログだけ残す」といった細かい設計が重要になります。

例外の親子関係と catch の順番

例外クラスには親子関係があります。 例えば NumberFormatExceptionIllegalArgumentException の子クラスで、さらにその上に RuntimeException があります。

catch は上から順番に評価されるので、「より具体的な例外」を先に書き、「より抽象的な例外」を後ろに書くのが鉄則です。

try {
    // 何か処理
} catch (NumberFormatException e) {
    // 具体的な対応
} catch (RuntimeException e) {
    // その他の実行時例外の対応
}
Java

もし先に親クラスを catch してしまうと、子クラスの catch に到達しなくなります。 「どの例外をどのレイヤで受け止めるか」「どこまで上に投げるか」は、APIレイヤ設計の重要なテーマです。

finally の役割(後始末を「必ず」やる場所)

finally は「例外があってもなくても」実行される

finally ブロックは、「例外が起きても起きなくても、最後に必ず実行される」場所です。

try {
    System.out.println("処理開始");
    int value = Integer.parseInt("123");
    System.out.println("処理成功: " + value);
} catch (NumberFormatException e) {
    System.out.println("処理失敗: " + e.getMessage());
} finally {
    System.out.println("後始末処理を実行します");
}
Java

このコードは、文字列が数値として正しくても、そうでなくても、最後に finally が必ず実行されます。 「後始末」「リソース解放」「接続のクローズ」など、「何があってもやらなきゃいけないこと」を書く場所が finally です。

リソース解放の典型パターン

例外処理で一番よく出てくるのが「ファイルやネットワーク接続などのリソースを必ず閉じる」パターンです。

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class FinallyExample {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("data.txt"));
            String line = reader.readLine();
            System.out.println("1行目: " + line);
        } catch (IOException e) {
            System.out.println("ファイル読み込みエラー: " + e.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                    System.out.println("ファイルを閉じました");
                } catch (IOException e) {
                    System.out.println("ファイルクローズ時のエラー: " + e.getMessage());
                }
            }
        }
    }
}
Java

ここでは、読み込み中にエラーが起きても、起きなくても、最後に reader.close() を呼んでファイルを閉じています。 「開いたら必ず閉じる」「接続したら必ず切る」という後始末を、例外の有無に関係なく保証するのが finally の役割です。

最近の Java では try-with-resources という書き方で、リソース解放をもっと簡潔に書けますが、「finally で後始末を書く」という基本の考え方は変わりません。

例外を「握りつぶす」危険と、設計のポイント

何もせず catch するのはほぼバグ

初心者がやりがちな危険なパターンがこれです。

try {
    int value = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    // 何もしない
}
Java

コンパイルエラーを消したくて、とりあえず try / catch を書いて、catch を空にしてしまう。 これは「問題が起きたことを完全に無視する」ことになり、後から原因が追えなくなります。

最低限、「ログを出す」「ユーザーに知らせる」「上位レイヤに投げ直す」など、何かしらの対応を入れるべきです。

catch (NumberFormatException e) {
    System.err.println("数値変換に失敗しました: " + e.getMessage());
    throw e; // そのまま上に投げる、という選択肢もある
}
Java

「このレイヤで責任を持って処理するのか」「上位レイヤに判断を委ねるのか」を意識して、例外の扱いを設計することが大事です。

「落とさない」だけでなく「意味のある失敗」にする

例外処理の目的は「落とさない」だけではありません。 ビジネス的に意味のある失敗に変換することも重要です。

例えば、「外部API呼び出しが失敗したら、ユーザーには『現在サービスが混み合っています』と伝える」「ログには詳細なエラー内容を残す」といった設計です。

try {
    callExternalService();
    System.out.println("処理が正常に完了しました");
} catch (IOException e) {
    System.out.println("現在サービスが混み合っています。しばらくしてから再度お試しください。");
    logError(e); // 内部向けログ
}
Java

「技術的な例外」を「ユーザーにとって意味のあるメッセージ」に変換する。 これが、APIレイヤやサービスレイヤでの例外処理設計の肝です。

まとめと小さな練習

例外処理は、システムの「安定性」と「後始末」を支える重要な仕組みです。 try で危ない処理を囲い、catch でどう振る舞うかを決め、finally で必ずやるべき後始末を書く。

練習として、次のようなコードを自分で書いてみると感覚がつかめます。

存在しないファイルを開こうとして IOException を catch し、ユーザー向けメッセージとログ出力を分ける 文字列の配列をループしながら Integer.parseInt を呼び、変換できないものだけエラーメッセージを出してスキップする

実際に「例外を起こしてみる」「catch でどう振る舞うかを自分で決める」「finally で後始末を書く」を体験すると、例外処理が「怖いもの」から「設計できる道具」に変わっていきます。

タイトルとURLをコピーしました