Java | Java 標準ライブラリ:RuntimeException を使う判断

Java Java
スポンサーリンク

RuntimeException を一言でいうと

RuntimeException は、

“呼び出し側に明示的な例外処理を強制したくはないが、異常はちゃんと知らせたい” ときに使う例外の親クラス

です。

  • try-catchthrows をコンパイラに強制させたくない
  • でも「起きたら無視していい」わけではなく、きちんと失敗として扱いたい

そういうときに、RuntimeException(もしくはそのサブクラス)を使う選択が出てきます。

大事なのは、「チェック例外がめんどくさいから RuntimeException にする」ではなく、
設計として RuntimeException に“意味”を持たせることです。


まず前提:RuntimeException とはどういう例外か

例外階層の中での位置づけ

Java の例外ツリーをざっくり書くとこうなります。

Throwable
Error
Exception
RuntimeException(そのサブクラスたち)

RuntimeException とそのサブクラスは「非チェック例外(unchecked)」と呼ばれます。

非チェック例外の特徴は、

メソッド宣言で throws 不要
呼び出し側は try-catch を強制されない
コンパイルは通るが、実行時に飛ぶ

ということです。

代表的な標準の RuntimeException は、

NullPointerException
IllegalArgumentException
IndexOutOfBoundsException
ArithmeticException

などで、
多くは「プログラマーのバグ」「事前条件違反」を表します。

チェック例外と何が違うのか

IOExceptionSQLException のようなチェック例外は、

「ファイルがない」「ネットワークが切れた」など、
真面目にやっても普通に起こる失敗を表します。

だからコンパイラは、

throws するか try-catch するか、どちらかを必ず書いてくれ

と迫ってきます。

一方、RuntimeException は、

null チェックしてれば起きない
インデックス計算が正しければ起きない

という「事前に防げる失敗」が多いので、
コンパイラは「そこまでは面倒見ない。自分で気をつけてね」というスタンスです。


RuntimeException を使うべき典型的な場面

1. 呼び出し側の使い方が間違っている(事前条件違反)

最も典型的なのが、「メソッドの前提条件を破っている」ケースです。

例えば、「年齢は 0 以上」というルールがあるメソッド。

public class User {
    private int age;

    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("年齢は 0 以上である必要があります: " + age);
        }
        this.age = age;
    }
}
Java

ここでのポイントは、

  • これは外部環境(ファイルやネットワーク)ではなく、呼び出し側のロジックの問題
  • 呼び出し元が「正しい値を渡す」という前提で API を設計したい

ということです。

もしこれをチェック例外にして、

public void setAge(int age) throws InvalidAgeException {
    ...
}
Java

とすると、呼び出し側は毎回 try-catch か throws を強制されます。

しかしこの例は、本来「呼び出し側がバグを直すべき話」であり、
例外処理で「復旧」するタイプの問題ではありません。

こういう「仕様違反の使い方を検出する」場合、
IllegalArgumentException のような RuntimeException を使うのが自然です。

2. 起きた時点で「続行しても意味がない」致命的状態

例えば「アプリの設定ファイルが壊れている」など、
いったん起きたら「ユーザーに直してもらうしかない」種類の問題です。

import java.io.IOException;
import java.nio.file.*;

public class AppConfig {
    private final String value;

    public AppConfig(Path path) {
        try {
            this.value = Files.readString(path);
        } catch (IOException e) {
            throw new RuntimeException("設定ファイルの読み込みに失敗しました: " + path, e);
        }
    }

    public String getValue() {
        return value;
    }
}
Java

ポイントは、

  • 内部でチェック例外(IOException)が発生しうる
  • しかし、このクラスを使う側から見れば、「設定が読めないならもうアプリを起動すべきではない」

という判断です。

この場合、わざわざ AppConfig のコンストラクタに throws IOException を付けて、
上のレイヤーで「設定未読のまま継続しようとする」よりも、

「RuntimeException で起動時に落ちる」方が設計として誠実なことも多いです。

つまり、

この例外が起きたら、ビジネス的に続行不能
→ RuntimeException にラップして落ちて構わない

という判断軸です。


「RuntimeException でラップする」か「チェック例外のまま伝える」か

ラップする典型例:DAO 層での SQLException

DB アクセス層(DAO)でチェック例外を RuntimeException にラップする例です。

public class DataAccessException extends RuntimeException {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}
Java
public class UserRepository {

    public User findById(String id) {
        try {
            // JDBC で SELECT 実行(SQLException が出る可能性)
        } catch (SQLException e) {
            throw new DataAccessException("ユーザー取得に失敗しました: id=" + id, e);
        }
    }
}
Java

ここでの設計意図は、

  • アプリのサービス層に、JDBC の細かい SQLException を意識させたくない
  • 「永続化層で何か起きた」という意味の RuntimeException にまとめたい
  • エラー発生時は、グローバルな例外ハンドラでログを取り、レスポンスを組み立てる

というものです。

これにより、サービス層のメソッドは throws SQLException に汚染されず、
「業務ロジックに集中したシンプルなシグネチャ」を保てます。

あえてチェック例外のまま伝える方が良いケース

一方、「呼び出し側が真剣にリトライやフォールバックを検討するべき」エラーでは、
チェック例外のままにしておいた方が親切です。

例えば「HTTP リクエストを送るライブラリ」を作るとき、
ネットワークエラーやタイムアウトをどう扱うか。

  • そのライブラリのユーザーに、必ずエラーハンドリングを意識してもらいたい
  • 単に落とすのではなく、「リトライする?諦める?」を呼び出し側で決めてほしい

という場合、
チェック例外として設計するのが良い選択になります。

RuntimeException でラップすると、

呼び出し側が例外処理を忘れていてもコンパイルが通ってしまう
→ 実行時にしか問題が分からない

という状態を招きやすいからです。


RuntimeException を使うときに絶対意識してほしいこと

「チェック例外が面倒だから RuntimeException」は危険

よくある悪いパターンが、

「IOException とか SQLException とか、throws が鬱陶しいから、全部 RuntimeException でラップしちゃえ」

というやり方です。

これは短期的にはコーディングが楽に見えるかもしれませんが、

  • どこで本気でエラー処理すべきか
  • どこでユーザーにまともなメッセージを返すべきか

という設計の考慮を「先送りにしている」だけです。

結果として、

  • なぜ落ちているのか分からない RuntimeException があちこちで飛ぶ
  • 呼び出し側は「起こるかもしれない失敗」をコード上で意識していない

という、すごくデバッグしづらいシステムになります。

RuntimeException を選ぶときは、

「呼び出し側に、コンパイル時に強制したくない」
「それでも起きたときに落ちて構わないか」

を意識的に自問してください。

catch RuntimeException して握りつぶすのはもっと危険

これもありがちです。

try {
    dangerousOperation();
} catch (RuntimeException e) {
    e.printStackTrace();
}
Java

あるいは、何もせず無視する。

これは「バグで落ちるべき場所を、見えなくしている」だけで、
システムの健全性を確実に悪化させます。

RuntimeException を catch するなら、

  • ログを記録して再スローする
  • システムを安全に停止させる
  • ユーザーに「内部エラー」を伝えるレスポンスを作る

など、「何をしたいのか」が明確であるべきです。

「とりあえず catch しておく」は、ほぼ確実に悪手です。


具体例で判断を練習する

例1:ユーザー入力のバリデーション失敗

ユーザー名は 1〜20 文字、半角英数字のみ、というルールがあるとします。

ここで「どの層で何を投げるか?」を考えてみます。

UI 層(フォーム入力を受け取るところ)
→ バリデーション失敗は「普通に起こる」ので、例外ではなく戻り値でエラーを返すことも多い

ドメイン層(User エンティティのコンストラクタなど)
→ 「この層では“不正な名前の User は存在しない”」という前提にしたい
→ 不正な値が来たら RuntimeException(例えば IllegalArgumentException)を投げる

このように、

ユーザー入力のエラー処理は UI 層で
ドメイン層は「ルールを破る入力はそもそも渡ってこない」前提

という役割分担をすることが多いです。

その上で、
「ドメイン層に不正値が来たら、それは UI 層かサービス層のバグ」とみなし、
RuntimeException で落として構わない、と判断します。

例2:ファイルの読み込み失敗

ConfigLoader というユーティリティクラスがあるとします。

アプリ起動時に一度だけ設定ファイルを読み込む想定です。

このとき、

設定ファイルが無い/読めない場合、
アプリを起動させるべきか?
それとも、起動前に落としてしまうべきか?

設計として「読めないなら起動すべきではない」と決めるなら、

内部で IOException を catch → RuntimeException にラップしてスロー

という選択が合理的です。

逆に、「設定ファイルがなくてもデフォルト設定で起動したい」なら、

IOException を catch してデフォルト値にフォールバック
→ 例外を外に出さない

という設計になります。

ここでも重要なのは、

RuntimeException にするかどうかは、「アプリをどう振る舞わせたいか」の設計の話

だということです。


まとめ:RuntimeException を使う判断の軸を自分の中に持つ

RuntimeException を初心者向けに整理すると、こうなります。

呼び出し側に例外処理を強制したくないが、起きたら基本的にはバグか致命的異常として扱いたい状況で使う例外

その判断軸として、次を自分に問いかけてください。

このエラーは、
外部要因で「普通に起こりうる」ものか?
それとも、プログラムの書き方や設計ミスに近いものか?

このエラーが起きたとき、
呼び出し側が“回復処理”を書く余地があるか?
それとも、落ちて構わない(あるいは落ちるべき)種類か?

前者ならチェック例外(または戻り値で扱う)の検討を、
後者なら RuntimeException 系の検討をする価値があります。

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