本ページはプロモーションが含まれています

Java入門

Java検査例外とは?try-catch/throwsの使い分けとベストプラクティス【2026年版】

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

Javaプログラミングを学び始めると、多くの人が「検査例外」の壁にぶつかります。コンパイラに「例外処理をしろ」と怒られ、意味も分からずtry-catchで囲んだ経験はありませんか。

実は、私もJava開発を始めたばかりの頃はそうでした。SES時代にチームでcatch (Exception e) {}の空キャッチを量産し、本番でサイレント障害を踏み抜いた苦い記憶もあります。10年以上の開発経験を積んだ今だからこそ断言できますが、検査例外はJavaの堅牢性を支える非常に優れた仕組みです。強制されるルールではなく、その設計思想を理解すれば、コードの品質を格段に向上させる武器になります。

この記事を読めば、あなたは以下の悩みを解決できます。

  • Javaの検査例外とは何か、なぜ必要なのかが分かる
  • try-catchthrowsの正しい使い分けが理解できる
  • 現場で役立つ例外処理のベストプラクティスが身につく
  • ラムダ式・ストリームAPIやSpring Bootでの現代的な例外設計の勘所が分かる

検査例外とは何か

最初に、Javaの検査例外の基本的な概念を解説します。なぜこのような仕組みが存在するのか、その設計思想まで掘り下げてみましょう。

Javaにおける例外の種類(検査例外と非検査例外)

Javaの例外は、大きく分けて「検査例外」と「非検査例外」の2種類に分類されます。

検査例外と非検査例外を分ける最も大きな違いは、コンパイラが処理を強制するかどうかです。

検査例外

  • コンパイル時にチェックされ、プログラマに例外処理を強制します
  • Exceptionクラスのサブクラス(RuntimeExceptionを除く)がこれにあたります
  • 回復可能性のある、予期すべきエラー通知に使われます

非検査例外

  • コンパイル時にチェックされず、処理を強制されません
  • RuntimeExceptionErrorの2種類があります
  • RuntimeException: プログラムのバグなど、主にプログラマのミスが原因です
  • Error: システムの致命的な問題で、プログラム側では回復不可能な状況を示します

検査例外の定義と特徴

検査例外の最大の特徴は、メソッド内で発生する可能性がある場合、必ず対処しなければならない点です。対処法は以下の2つしかありません。

ポイント

  1. try-catchブロックで例外を捕捉し、その場で処理する
  2. throws宣言を使い、メソッドの呼び出し元に例外処理の責任を転嫁する

どちらかの対応をしないと、コンパイルエラーとなりプログラムを実行できません。これは、開発者に対して「この処理は失敗する可能性があるから、きちんと備えておきなさい」というJavaからのメッセージなのです。

なぜ検査例外が存在するのか

では、なぜJavaはわざわざ検査例外という強制的な仕組みを導入したのでしょうか。

その理由は、堅牢で信頼性の高いプログラムを作るためです。

ファイル操作やネットワーク通信など、プログラムの外部環境に依存する処理は、成功が保証されません。たとえば、読み込もうとしたファイルが存在しないケースや、接続先のサーバがダウンしているケースが代表例です。

このような「起こりうる問題」を開発者に意識させ、事前に対処コードを書かせるのがJava検査例外の狙いです。例外処理を強制することで、プログラムが予期せぬエラーで突然停止してしまう事態を防ぎ、より安定したアプリケーション開発を促しています。

代表的な検査例外の例

ここでは、実際の開発でよく目にする代表的な検査例外を3つ紹介します。これらの例外がどのような場面で発生するのかを知っておくと、エラー発生時の対応がスムーズになります。

IOException

IOExceptionは、データの入出力(I/O)処理で問題が発生した場合にスローされる検査例外です。Javaの標準APIで最も頻繁にスローされる検査例外のひとつで、以下のような場面で登場します。

  • ファイルの読み書き(FileReader, FileWriter
  • ネットワーク通信(Socket, URL
  • 外部プロセスとのやりとり

例えば、存在しないファイルを読み込もうとした場合や、書き込み先のディスク容量が不足している場合などに発生します。これらの事態はプログラムの実行中に起こりうるため、開発者はIOExceptionを処理し、エラー発生時の代替処理を実装する必要があります。

SQLException

SQLExceptionは、データベース連携時に発生する問題を通知するための検査例外です。JDBC APIを利用してデータベースを操作する際に発生します。

  • データベースへの接続失敗
  • 実行したSQL文の構文エラー
  • 一意制約違反(重複したデータを登録しようとした場合など)

業務システムの多くはデータベースを利用するため、SQLExceptionの処理はJavaでのシステム開発に不可欠です。トランザクション管理と組み合わせ、データの一貫性を保つための重要な役割を担います。

その他のよく使われる検査例外

上記2つの他にも、特定の場面で使われる重要な検査例外が存在します。

  • ClassNotFoundException: クラスパス上に対象のクラスが見つからない場合に発生します。リフレクション(プログラム実行時にクラス情報を動的に扱う仕組み)などでよく見られます。
  • InterruptedException: あるスレッド(処理の実行単位)が待機、スリープ、またはその他の方法で占有されている間に、別のスレッドから割り込みがかけられた場合にスローされます。マルチスレッドプログラミングで重要な例外です。

検査例外の使い方と注意点

検査例外を正しく扱うための具体的な方法と、守るべきベストプラクティスを解説します。コードの品質を左右する重要なポイントです。

throws宣言とtry-catchの違い

検査例外の処理方法はthrowstry-catchの2つですが、それぞれ役割が異なります。

  • try-catch: その場で例外を処理する方法です。例外が発生する可能性のあるコードをtryブロックで囲み、発生した例外をcatchブロックで捕捉して対処します。エラー回復処理を自身で行う場合に選択します。
public void readFile() {
    File file = new File("test.txt");
    try {
        FileReader reader = new FileReader(file);
        // ファイルを読み込む処理...
    } catch (FileNotFoundException e) {
        // 例外をここでキャッチして処理する
        System.err.println("ファイルが見つかりませんでした。");
        // スタックトレースを出力してデバッグに役立てる
        e.printStackTrace();
    }
}
  • throws: 例外処理の責任を呼び出し元に転嫁する方法です。メソッドのシグネチャ(メソッド名や引数の情報)にthrowsキーワードと例外クラスを記述します。これにより、このメソッドを利用する側が例外処理を強制されます。
// 呼び出し元にFileNotFoundExceptionの処理を委譲する
public FileReader readFile() throws FileNotFoundException {
    File file = new File("test.txt");å
    FileReader reader = new FileReader(file);
    return reader;
}

// 呼び出し側
public void processFile() {
    try {
        FileReader reader = readFile();
        // 処理を続ける...
    } catch (FileNotFoundException e) {
        System.err.println("ファイルの処理中にエラーが発生しました。");
    }
}

メソッド内でエラーに対処できる場合はtry-catchを、対処できず呼び出し元に判断を委ねたい場合はthrowsを使うのが基本です。

複数の検査例外を処理する方法

1つのtryブロック内で、複数の種類の検査例外が発生する可能性があります。その場合、2つの書き方があります。

1つ目は、catchブロックを複数記述する方法です。例外の種類ごとに異なる処理を行いたい場合に有効です。

try {
    // 複数の検査例外が発生しうる処理
} catch (IOException e) {
    // IOException発生時の処理
} catch (SQLException e) {
    // SQLException発生時の処理
}

2つ目は、マルチキャッチです。複数の例外に対して同じ処理を行いたい場合、コードを簡潔に記述できます。

try {
    // 複数の検査例外が発生しうる処理
} catch (IOException | SQLException e) {
    // IOExceptionまたはSQLExceptionが発生した場合の共通処理
}

try-with-resources構文による自動リソース解放

Java 7で導入されたtry-with-resources構文は、検査例外処理の現代的なベストプラクティスです。AutoCloseableを実装したリソースを宣言すると、例外の有無にかかわらず自動でclose()が呼ばれ、リソースリークを防ぎます。

// try-with-resources(推奨)
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
    return reader.readLine();
} catch (IOException e) {
    logger.error("ファイル読み込みに失敗", e);
    throw new UncheckedIOException(e);
}
// finallyでclose()を呼ぶ旧来パターンに比べ、例外抑制(suppressed exception)も自動で扱われる

従来のfinallyでのクローズ処理は、close()自体が例外を投げた場合に本来の例外が上書きされるという罠がありました。try-with-resourcesはこの問題も自動で解決してくれるため、2026年時点の実務コードではデフォルトで採用すべき書き方です。

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

質の高い例外処理を実装するための、3つの重要なベストプラクティスを紹介します。

  1. catchブロックを空にしない最もやってはいけないのが、catchブロックを空にすることです。これは「エラーの握りつぶし」と呼ばれ、問題が発生しても何も通知されず、原因調査が極めて困難になります。最低限、ログに出力するようにしましょう。
  2. 具体的な例外クラスをキャッチするcatch (Exception e)のように、すべての例外の親であるExceptionクラスで捕捉するのは避けるべきです。予期せぬRuntimeExceptionまで捕捉してしまい、本来修正すべき問題を見逃す原因になります。捕捉したい例外クラスを具体的に指定しましょう。
  3. 例外情報を失わない(例外のラップ)下位の例外を捕捉し、より上位の抽象的な例外(独自の業務例外など)でラップして再スローすることがあります。その際は、必ずコンストラクタの第2引数に元の例外を渡すか、initCause()causeを設定して元の情報を引き継ぎます。ログ出力時もlogger.error("msg", e)のようにThrowableごと渡す運用にしてください。
// 悪い例: 元の例外情報が失われる
catch (SQLException e) {
    throw new MySystemException("データベースエラー"); 
}
// 良い例: causeとして元の例外を渡す
catch (SQLException e) {
    throw new MySystemException("データベースエラー", e); // 第2引数で元の例外を渡す
} 

元の例外をcauseとして設定することで、根本原因のスタックトレースが残り、デバッグが容易になります。

検査例外と非検査例外の使い分け

理論を学んだところで、次は実践的な使い分けについて考えます。適切な例外設計は、APIの使いやすさやシステムの保守性に直結します。

適切な例外設計の考え方

検査例外と非検査例外を使い分ける際の最も重要な判断基準は、「呼び出し元(APIの利用者)が、そのエラーから回復できるか」です。

検査例外を使うべきケース

  • 呼び出し元がエラーから回復できる、あるいは代替処理を行うことが期待される場合。
  • 例: 「指定されたファイルが存在しない(FileNotFoundException)」→ 呼び出し元は「デフォルト設定で処理を続行する」「ユーザーに別ファイルの選択を促す」などの対応が考えられる。

非検査例外(RuntimeException)を使うべきケース

  • プログラムの事前条件を満たしていないなど、明らかにプログラマのバグが原因の場合。
  • 例: nullが渡されるべきでない引数にnullが渡された(NullPointerException)。これは呼び出し側のコードを修正すべき問題であり、その場で回復しようとするべきではない。

APIを設計する際は、「このエラーが起きたとき、API利用者にどうしてほしいか?」を自問自答することが、適切な例外選択の鍵となります。

業務システム開発における検査例外の実例

業務システムでは、独自の検査例外を定義することがよくあります。

筆者が関わったSpring Bootの会計系システムでも、「仕訳バランスエラー」「会計期間締め後の更新」など業務ルール違反は独自の検査例外として定義し、Controllerで@ExceptionHandlerに拾わせる設計にしていました。「予期すべきエラー」を検査例外で型として明示しておくと、コードレビュー時に処理漏れを発見しやすく、実装者が業務ルールを忘れにくいという副次効果もあります。

例えば、銀行の振込処理を考えてみましょう。「残高不足」はエラーですが、システム障害ではなく業務ルール上の正常な結果です。このような場合、InsufficientBalanceExceptionという独自の検査例外を定義します。

public void transfer(Account from, Account to, long amount) throws InsufficientBalanceException {
    if (from.getBalance() < amount) {
        // 残高不足は業務上の例外として通知
        throw new InsufficientBalanceException("残高が不足しています。");
    }
    // 振込処理...
}

このメソッドの呼び出し元は、InsufficientBalanceExceptioncatchして、「残高が不足しています」というメッセージをユーザーに表示する処理を実装できます。このように、業務上のルールに関わるエラーは、検査例外で表現するのが効果的です。

Spring Bootでの検査例外とグローバル例外処理

Spring Bootで業務アプリを作る場合、@ControllerAdvice@ExceptionHandler を使ったグローバル例外処理が定番です。検査例外はサービス層でビジネス例外として投げ、Controller層では握らず、@ControllerAdvice でHTTPレスポンスに変換するのが定石です。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(InsufficientBalanceException.class)
    public ResponseEntity<ErrorResponse> handle(InsufficientBalanceException e) {
        return ResponseEntity.badRequest()
            .body(new ErrorResponse("BALANCE_SHORT", e.getMessage()));
    }
}

この構造を採用すると、Controllerごとにtry-catchを書く必要がなくなり、横断的関心事(ロギング・監査・エラーレスポンスの統一)を一箇所に集約できます。実務のSpring Boot案件では、ビジネス例外は検査例外で定義し、インフラ例外(DB接続断など)は非検査例外で扱う、という住み分けが主流です。

検査例外を避けるべきケース

検査例外は強力な仕組みですが、現代のJavaプログラミング、特にラムダ式やストリームAPIと組み合わせる際には、扱いにくさが問題になることがあります。

ラムダ式・ストリームAPIと検査例外の相性問題

Java 8(2014年リリース)で導入されたストリームAPIやラムダ式は、2026年時点でも日常的に使われる主力機能です。しかし、これらの多くが受け入れる関数型インタフェース(例: Function, Predicate)のメソッドシグネチャにはthrows宣言が含まれていないため、ラムダ式内で検査例外を直接スローできません。

そのため、ラムダ式の本体から検査例外をスローしようとすると、コンパイルエラーになってしまいます。

List<String> fileNames = List.of("file1.txt", "file2.txt");

// コンパイルエラー!
// ラムダ式内でIOExceptionをスローできない
fileNames.stream()
         .map(name -> new FileReader(name)) // FileReaderのコンストラクタはFileNotFoundExceptionをスローする
         .collect(Collectors.toList());

この「相性の悪さ」は、Javaコミュニティでも長年議論されているテーマです。

ラップしてRuntimeExceptionに変換するパターン

この問題を解決する一般的なパターンが、検査例外をtry-catchで捕捉し、非検査例外でラップして再スローする方法です。

fileNames.stream()
    .map(name -> {
        try {
            return new FileReader(name);
        } catch (FileNotFoundException e) {
            // 検査例外を非検査例外でラップしてスローする
            throw new UncheckedIOException(e); // Java 8で追加された便利な非検査例外
        }
    })
    .collect(Collectors.toList());

この方法を使えば、ラムダ式内で検査例外を扱うことができ、コードの記述がスムーズになります。

ただし、このパターンを乱用すると、検査例外が持つ「回復可能なエラーを呼び出し元に通知する」という本来のメリットが失われてしまいます。あくまで、ラムダ式など構造上やむを得ない場合に限定して利用するのが賢明です。なお、Java 8以降に追加されたUncheckedIOExceptionのほか、Spring Framework系ではDataAccessExceptionのようにチェック例外を非チェック例外に変換するラッパが標準で用意されており、手動でラップする前に既存ユーティリティの有無を確認するのがおすすめです。

Java検査例外に関するよくある質問

検査例外は時代遅れですか?

Kotlinなど新しいJVM言語は検査例外を採用していないため「時代遅れ」と語られることがあります。ただしJava本体では2026年時点でも仕様変更の予定はなく、IOExceptionSQLExceptionなど標準APIの大部分が依然として検査例外のままです。業務例外の一部に限って検査例外を使い、インフラ系は非検査例外に寄せる、というハイブリッド運用が現実解です。

throws Exception と書くのはアリですか?

学習用のスクリプトなら許容範囲ですが、業務コードではNGです。throws Exception は呼び出し元にすべての検査例外処理を強制し、例外ごとの意味を失わせます。RuntimeExceptionまで巻き込んで握りつぶすリスクもあるため、スローする例外は具体クラスで宣言するのが鉄則です。

Java Silver試験では検査例外はどう問われますか?

Java Silver (1Z0-830) では、検査例外/非検査例外の区別、throws宣言の継承時のルール、マルチキャッチの制約などが頻出です。特に「オーバーライド時に親メソッドより広い検査例外をthrows宣言できるか」は毎回出題される定番トピックなので、本記事の内容と合わせて押さえておくと得点源になります。

まとめ

最後に、この記事で解説してきた内容を振り返り、Java 21 LTSが主流となった2026年時点でも変わらず重要な「検査例外の正しい理解」を改めて整理します。

検査例外を正しく理解する重要性

Javaの検査例外は、単なるコンパイラのお節介ではありません。それは、プログラムの外部要因によって発生しうる「回復可能なエラー」を開発者に明示し、事前に対処を促すための重要な設計です。この仕組みを理解し、適切に利用することで、エラーに強い、信頼性の高いアプリケーションを構築できます。

最初は面倒に感じるかもしれませんが、「例外は型で語る」という設計思想を理解すれば、Javaの検査例外ほど読み手とコンパイラの両方に優しい機能はありません。

例外設計がコード品質に与える影響

例外設計は、アプリケーション全体の品質を大きく左右します。

  • 適切な例外処理は、問題の早期発見と迅速な原因特定を助けます。
  • 一貫性のある例外設計は、APIの利用者に正しい使い方をガイドし、コードの可読性と保守性を向上させます。
  • 特にチーム開発においては、共通の例外処理ルールを設けることが、プロジェクト全体の成功に繋がります。
  • この記事を書いた人
  • 最新記事

トム

・都内自社開発企業勤務/Javaバックエンドエンジニア
/Java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎 詳細なプロフィール

-Java入門