Java例外処理完全マスター

2025-08-02

はじめに

Javaプログラミングにおいて、例外処理はプログラムの堅牢性と信頼性を確保するための重要な技術です。この記事では、Javaの例外処理について、基本構文から実践的な活用テクニックまで、段階的に詳しく解説します。すでにJavaの基本文法を理解していることを前提として、例外処理に特化した内容をお届けします。

例外処理を学ぶメリット:

  • 予期せぬエラーに対処できる堅牢なプログラムを作成可能
  • プログラムのクラッシュを防ぎ、ユーザーフレンドリーな体験を提供
  • デバッグやトラブルシューティングが容易になる
  • リソース管理(ファイル、DB接続など)を安全に行える

例外処理の基本概念

例外とは何か?

例外とは、プログラムの正常な実行フローを妨げる異常な状態やイベントのことです。例えば、ファイルが見つからない、ネットワーク接続が切れる、配列の範囲外にアクセスするなどの状況が該当します。

Javaの例外クラス階層

Javaの例外はすべてThrowableクラスのサブクラスです。主要なクラス階層は以下の通りです。

Throwable
├── Error (システムレベルの深刻な問題)
└── Exception
    ├── RuntimeException (実行時例外)
    └── その他の検査例外 (Checked Exception)
exception-handling

検査例外(Checked Exception) vs 非検査例外(Unchecked Exception)

特徴検査例外 (Checked Exception)非検査例外 (Unchecked Exception)
継承関係Exceptionのサブクラス(RuntimeException除く)RuntimeExceptionまたはErrorのサブクラス
コンパイル時のチェックあり(処理必須)なし(処理任意)
IOException, SQLExceptionNullPointerException, ArrayIndexOutOfBoundsException
使用場面回復可能な状況プログラミングエラー

基本的な例外処理構文

try-catchブロック

try {
    // 例外が発生する可能性のあるコード
} catch (例外型1 変数名) {
    // 例外型1に対する処理
} catch (例外型2 変数名) {
    // 例外型2に対する処理
}

具体例:ファイル読み込み

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

public class TryCatchExample {
    public static void main(String[] args) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader("test.txt"));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("ファイル読み込みエラー: " + e.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.err.println("ファイルクローズエラー: " + e.getMessage());
                }
            }
        }
    }
}

try-with-resources構文 (Java 7以降)

リソース管理を自動化する簡潔な構文です。

try (リソース宣言) {
    // リソースを使用するコード
} catch (例外型 変数名) {
    // 例外処理
}

具体例:自動リソース管理

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

public class TryWithResourcesExample {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("エラー発生: " + e.getMessage());
        }
        // reader.close()が自動で呼ばれる
    }
}

finallyブロック

例外の有無に関わらず実行されるクリーンアップコードを記述します。

try {
    // リソース使用
} catch (例外型 e) {
    // 例外処理
} finally {
    // 必ず実行されるコード(リソース解放など)
}

例外の種類と対処法

よく遭遇するRuntimeException

  1. NullPointerException
   String str = null;
   System.out.println(str.length()); // NullPointerException
  1. ArrayIndexOutOfBoundsException
   int[] arr = new int[5];
   System.out.println(arr[5]); // 範囲外アクセス
  1. ClassCastException
   Object obj = "Hello";
   Integer num = (Integer) obj; // 型変換失敗
  1. IllegalArgumentException
   public void setAge(int age) {
       if (age < 0) {
           throw new IllegalArgumentException("年齢は正の値でなければなりません");
       }
       this.age = age;
   }

よく使われる検査例外

  1. IOException
   try {
       Files.readAllLines(Paths.get("nonexistent.txt"));
   } catch (IOException e) {
       System.err.println("ファイルアクセスエラー: " + e.getMessage());
   }
  1. SQLException
   try {
       Connection conn = DriverManager.getConnection(url);
       Statement stmt = conn.createStatement();
       ResultSet rs = stmt.executeQuery("SELECT * FROM users");
   } catch (SQLException e) {
       System.err.println("データベースエラー: " + e.getMessage());
   }

例外のスローとカスタム例外

例外のスロー

throwキーワードで明示的に例外を発生させられます。

public double divide(double a, double b) {
    if (b == 0) {
        throw new ArithmeticException("0で除算することはできません");
    }
    return a / b;
}

カスタム例外の作成

アプリケーション固有の例外を作成するには、Exceptionクラスを継承します。

// 検査例外バージョン
public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }

    public InsufficientFundsException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 非検査例外バージョン
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

使用例

public class BankAccount {
    private double balance;

    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException(
                "残高不足: 現在の残高 " + balance + ", 引き出し額 " + amount);
        }
        balance -= amount;
    }
}

例外処理のベストプラクティス

1. 具体的な例外をキャッチする

// 悪い例
try {
    // 何らかの処理
} catch (Exception e) {
    // すべての例外をキャッチ
}

// 良い例
try {
    // ファイル操作
} catch (FileNotFoundException e) {
    // ファイルが見つからない場合の処理
} catch (IOException e) {
    // その他のI/Oエラー処理
}

2. 例外のラッピング

try {
    // 何らかの処理
} catch (SpecificException e) {
    throw new MyApplicationException("コンテキスト情報を追加", e);
}

3. リソース管理にはtry-with-resourcesを使用

// 悪い例(手動クローズ)
BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("file.txt"));
    // ファイル操作
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            // クローズエラー処理
        }
    }
}

// 良い例(try-with-resources)
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
    // ファイル操作
} catch (IOException e) {
    // 例外処理
}

4. 例外メッセージを有益にする

// 悪い例
throw new IllegalArgumentException("無効な引数");

// 良い例
throw new IllegalArgumentException(
    String.format("無効な値: %d。値は0から100の間でなければなりません", value));

5. ロギングを適切に行う

try {
    // リスクのある操作
} catch (BusinessException e) {
    logger.error("業務処理に失敗しました。ユーザーID: {}, 操作: {}", userId, operation, e);
    throw e;
}

実践的な例外処理パターン

リトライメカニズム

public static  T retry(Callable task, int maxRetries, long delayMs) 
        throws Exception {
    int retries = 0;
    while (true) {
        try {
            return task.call();
        } catch (Exception e) {
            if (++retries >= maxRetries) {
                throw e;
            }
            Thread.sleep(delayMs);
        }
    }
}

// 使用例
String result = retry(() -> {
    // ネットワーク呼び出しなど失敗する可能性のある操作
    return httpClient.get("https://api.example.com/data");
}, 3, 1000);

例外変換パターン

public class UserRepository {
    public User findById(String id) throws UserNotFoundException {
        try {
            // DB操作
            return dao.findById(id);
        } catch (SQLException e) {
            throw new UserNotFoundException("ユーザーが見つかりません: " + id, e);
        }
    }
}

防御的プログラミング

public class PaymentProcessor {
    public void process(Payment payment) {
        Objects.requireNonNull(payment, "paymentはnullであってはなりません");

        if (payment.getAmount() <= 0) {
            throw new IllegalArgumentException(
                "支払額は正の値でなければなりません: " + payment.getAmount());
        }

        // 処理を続行
    }
}

よくある間違いと回避策

1. 例外の握り潰し

// 悪い例
try {
    // 何らかの処理
} catch (Exception e) {
    // 何もしない(例外を無視)
}

// 良い例
try {
    // 何らかの処理
} catch (SpecificException e) {
    logger.error("処理に失敗しました", e);
    // 必要に応じて回復処理や通知
}

2. 過剰な例外処理

// 悪い例
try {
    int[] arr = {1, 2, 3};
    System.out.println(arr[1]);
} catch (ArrayIndexOutOfBoundsException e) {
    // 通常は発生しない例外をキャッチ
}

// 良い例
int[] arr = {1, 2, 3};
if (arr.length > 1) {
    System.out.println(arr[1]);
}

3. 例外の不適切な再スロー

// 悪い例
try {
    // 何らかの処理
} catch (Exception e) {
    throw new MyException(e.getMessage()); // 元の例外情報が失われる
}

// 良い例
try {
    // 何らかの処理
} catch (Exception e) {
    throw new MyException("追加コンテキスト", e); // 原因例外を保持
}

例外処理のパフォーマンス考慮事項

例外処理のコスト

例外の生成と処理には以下のコストがかかります:

  1. スタックトレースの生成(特にコストが高い)
  2. 例外オブジェクトの作成
  3. 例外ハンドラの探索

パフォーマンスチップス

  • 通常の制御フローに例外を使用しない
  • スタックトレースが必要ない場合はThrowableのサブクラスでfillInStackTrace()をオーバーライド
  • 頻繁に発生するエラー条件は例外ではなく戻り値で処理

スタックトレース無効化の例

public class LightweightException extends RuntimeException {
    @Override
    public synchronized Throwable fillInStackTrace() {
        return this; // スタックトレースを生成しない
    }

    public LightweightException(String message) {
        super(message);
    }
}

Java 14以降の新しい例外機能

Helpful NullPointerExceptions

Java 14で導入された、より詳細なNullPointerExceptionメッセージ。

従来:
Exception in thread "main" java.lang.NullPointerException

Java 14以降:
Exception in thread "main" java.lang.NullPointerException: 
Cannot invoke "String.length()" because "str" is null

パターンマッチング for instanceof (Java 16)

// 従来の書き方
if (e instanceof IOException) {
    IOException ioEx = (IOException)e;
    // ioExを使用した処理
}

// Java 16以降
if (e instanceof IOException ioEx) {
    // ioExを直接使用
}

練習問題

理解を深めるための練習問題を用意しました。実際にコードを書いて試してみましょう。

問題1: 安全な除算メソッド

2つの整数を受け取り、除算結果を返すメソッドを作成してください。0除算の場合、カスタム例外DivisionByZeroExceptionをスローするようにしてください。

問題2: ファイルコピーツール

ソースファイルからターゲットファイルに内容をコピーするメソッドを作成してください。適切な例外処理を行い、リソースリークが起こらないようにしてください。

問題3: ユーザー入力検証

ユーザーから年齢(正の整数)を受け取るメソッドを作成してください。不正な入力の場合、適切な例外をスローし、エラーメッセージを表示してください。

問題4: リトライメカニズム

指定された回数まで処理をリトライするユーティリティメソッドを作成してください。リトライ間隔は指数バックオフで増加させるようにしてください。

問題5: 例外変換

データベースアクセス層で発生するSQLExceptionを、アプリケーション固有の例外(DataAccessException)に変換するラッパークラスを作成してください。

次のステップ

例外処理の基礎を理解したら、以下のトピックに進みましょう:

  1. アサーション - 開発中の内部チェック
  2. ロギングフレームワーク - 例外の適切な記録
  3. トランザクション管理 - 例外時のロールバック処理
  4. リアクティブプログラミング - 非同期処理におけるエラーハンドリング

さらに学ぶためのリソース:

まとめ

この記事では、Javaの例外処理について包括的に解説しました。重要なポイントをまとめます:

  1. try-catch-finally - 基本的な例外処理構文
  2. try-with-resources - リソース管理の簡潔な構文
  3. 検査例外 vs 非検査例外 - 適切な使い分け
  4. カスタム例外 - アプリケーション固有の例外定義
  5. 例外処理のベストプラクティス - 堅牢で保守性の高いコード作成

例外処理は品質の高いJavaアプリケーション開発に不可欠です。最初はシンプルな処理から始め、徐々に高度なパターンを習得していきましょう。適切な例外処理は、バグの早期発見と修正を助け、結果的に開発時間の短縮につながります。Happy coding!