Javaの例外処理とは?try-catchの書き方から実務対応まで解説

Mirai

例外処理って、なんとなくわかるけど実務でどう使えばいいのかわからない…。

Zetto

例外処理はプログラムがエラーで止まるのを防ぐ仕組みのことだね。基本の書き方さえ押さえれば、実務でも自然と使いこなせるようになるよ!

この記事では、Javaの例外処理の基礎から実務で使えるテクニックまで解説します。

Zettoのライタープロフィール

本記事の専門性
現役フリーランスエンジニアのZettoです。Java SilverとJava Goldの資格を持ち、JavaでWebアプリの案件を複数経験しています。

例外処理を理解しないまま進むと、エラーが出るたびにプログラムが止まり、デバッグに何時間も費やすことになりがちです。

この記事を読めば、try-catchの書き方・チェック例外と非チェック例外の違い・実務で使えるテクニックまで、一通り理解できます。

ぜひ参考にしてみてください。

目次

Java例外処理とは何か?プログラムが止まる前に知っておくべき基礎

Java例外処理の基礎

この章では、Javaの例外処理を理解するために必要な基礎知識を整理します。

  • 例外(Exception)とエラー(Error)の違い
  • 例外処理が必要な理由と発生する仕組み
  • チェック例外・非チェック例外・Errorの3種類を整理する

では順番に見ていきましょう。

例外(Exception)とエラー(Error)の違い

Javaには、プログラムの異常を表すクラスが2種類あります。それが「例外(Exception)」と「エラー(Error)」です。

大きな違いは、開発者が対処できるかどうかです。

  • 例外(Exception):プログラムの処理中に起こりうる問題。適切に処理することでプログラムを続行できる
  • エラー(Error):JVM(Javaの実行環境)レベルの深刻な問題。OutOfMemoryError(メモリ不足)などが代表例で、開発者が対処するのは現実的ではない

Javaのクラス階層では、Throwableクラスが頂点にあり、その下にExceptionとErrorがぶら下がっています。

実務ではErrorをcatchするコードはほぼ書きません。開発者が扱うのは基本的にExceptionの範囲です。

例外処理が必要な理由と発生する仕組み

例外処理が必要な理由はシンプルです。例外処理を入れないと、エラーが起きたときにプログラムが即座に停止するからです。

たとえば、ファイルを読み込む処理でファイルが存在しなかった場合、処理を止めて利用者にエラーを投げるのではなく、「ファイルが見つかりませんでした」と適切なメッセージを返せた方がずっと使いやすいですよね。

例外が発生する仕組みは次のような流れです。

  1. プログラムが例外の発生する処理を実行する
  2. JVMが例外オブジェクトを生成する
  3. 処理がtryブロックの外に出るまで「スタックトレース(エラーの発生経路を示すログ)」を持ちながら伝播する
  4. catchブロックで受け取るか、誰も受け取らなければプログラムが停止する

実務でスタックトレースを読む機会は多いです。慣れるほど原因の特定が速くなります。

チェック例外・非チェック例外・Errorの3種類を整理する

Javaの例外は大きく3つに分類できます。

  • チェック例外(Checked Exception):コンパイル時にハンドリングが強制される例外。IOExceptionやSQLExceptionが代表例
  • 非チェック例外(Unchecked Exception):実行時に発生し、ハンドリングが強制されない例外。NullPointerExceptionやArrayIndexOutOfBoundsExceptionが代表例。RuntimeExceptionのサブクラスに当たる
  • Error:JVMの深刻な問題。開発者がハンドリングすることは想定されていない

チェック例外かどうかは、RuntimeExceptionを継承しているかどうかで判断できます。RuntimeExceptionを継承していれば非チェック例外、していなければチェック例外です。

例外の種類については、Javaの公式APIドキュメントでも確認できます。

ここで基礎がつかめたと思います。次の章からはいよいよコードを書いていきましょう。

Zetto

「例外とエラーって何が違うの?」という疑問は、Javaを使い始めた頃の僕にも刺さる質問でした。整理してしまえばシンプルなので、ここで確実に押さえておきましょう。

try-catch-finallyの書き方を完全マスターする

try-catch-finally書き方

この章では、例外処理の基本構文であるtry-catch-finallyを丁寧に解説します。

  • 基本構文と処理の流れ(コード例つき)
  • 複数の例外を個別にキャッチする書き方
  • finallyブロックの役割と正しい使い方

では順番に確認していきましょう。

基本構文と処理の流れ(コード例つき)

try-catchの基本的な構文は次の通りです。

try {
    // 例外が発生するかもしれない処理
    int result = 10 / 0; // ArithmeticException が発生
} catch (ArithmeticException e) {
    // 例外をキャッチしたときの処理
    System.out.println("ゼロ除算が発生しました: " + e.getMessage());
}

処理の流れはこうなります。

  1. tryブロック内の処理を実行する
  2. 例外が発生すると、以降の処理はスキップされる
  3. 発生した例外の型に一致するcatchブロックが実行される
  4. catchブロックに渡されるe(例外オブジェクト)から、エラーメッセージや発生箇所の情報を取得できる

Javaの現場では、catchの中にe.printStackTrace()(スタックトレースをコンソールに出力するメソッド)だけ書いて終わりにしているケースがあります。

本来はそこから適切なレスポンスやログ出力を実装する必要があります。基本を理解してから現場に入ると、こういった点に気づきやすくなりますよ。

複数の例外を個別にキャッチする書き方

1つの処理から複数の例外が発生する可能性がある場合は、catchブロックを複数書けます。

try {
    String text = null;
    System.out.println(text.length()); // NullPointerException
    int[] arr = new int[3];
    arr[10] = 1;                       // ArrayIndexOutOfBoundsException
} catch (NullPointerException e) {
    System.out.println("nullが参照されました");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("配列の範囲外アクセスです");
} catch (Exception e) {
    // 上記以外の例外をまとめてキャッチする(最後に書く)
    System.out.println("予期しない例外: " + e.getMessage());
}

ポイントが1つあります。catchブロックは上から順に評価されるため、子クラス(より具体的な例外)を上に、親クラス(より汎用的な例外)を下に書く必要があります。

Java 7以降では、|(パイプ)で複数の例外をまとめて1つのcatchブロックに書くこともできます。

catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
    System.out.println("NullまたはArrayの例外が発生しました");
}

処理内容が同じなら、この書き方でコードをスッキリさせるのもありですね。

finallyブロックの役割と正しい使い方

finallyブロックは、例外が発生してもしなくても、必ず実行される処理を書く場所です。

try {
    // 何らかの処理
    System.out.println("処理実行");
} catch (Exception e) {
    System.out.println("例外発生: " + e.getMessage());
} finally {
    // 例外の有無に関わらず実行される
    System.out.println("finallyブロックが実行されました");
}

finallyの主な用途は、ファイルやデータベース接続などのリソースを閉じる処理です。

ただし、後述するtry-with-resources構文を使えば、finallyでリソースを手動クローズする書き方は不要になります。現在の現場ではfinallyを使う機会は減っていますが、レガシーコードの読解やOSSのコードを読むときに理解が必要になります。

try-catch-finallyの基本はこれで一通り押さえられます。コードを手元で書いて動かすと、理解がぐっと深まりますよ。

Mirai

catch (Exception e) って全部まとめてキャッチしていいんじゃないの?

Zetto

それをやると、どんなエラーが起きても同じ処理をしてしまうことになるんだよね。ファイルが存在しないのか、ネットワークが切れたのかで対応が変わるから、できるだけ具体的な例外型でキャッチするのが基本だよ。

チェック例外と非チェック例外の違いと使い分け方

チェック例外と非チェック例外

この章では、Javaの例外を実務で使いこなすうえで重要な「チェック例外と非チェック例外の使い分け」を解説します。

  • チェック例外の代表例と使われる場面
  • 非チェック例外の代表例と使われる場面
  • 現場での判断基準|どちらを投げるべきか

では順番に見ていきましょう。

チェック例外の代表例と使われる場面

チェック例外は、コンパイラが「この例外を処理しなさい」と強制してくれる例外です。

主な例は以下の通りです。

  • IOException:ファイルの読み書き・ネットワーク通信で発生する。外部リソースを扱う処理では必ず意識する
  • SQLException:データベース操作時に発生する。JDBCを直接使う場合に頻出
  • FileNotFoundException:指定したファイルが存在しない場合に発生。IOExceptionのサブクラス

チェック例外が発生するメソッドを呼ぶ場合は、必ずtry-catchで囲むか、throws宣言で呼び出し元に処理を委ねる必要があります。

これはコンパイルエラーになります

// コンパイルエラー(throws か try-catch が必要)
public void readFile(String path) {
    FileReader reader = new FileReader(path); // FileNotFoundException
}

こう書けばOKです。

public void readFile(String path) throws IOException {
    FileReader reader = new FileReader(path);
    // ファイルを読む処理...
}

非チェック例外の代表例と使われる場面

非チェック例外は、実行時(Runtime)に発生する例外で、コンパイラはハンドリングを強制しません。

主な例は以下の通りです。

  • NullPointerException:nullの参照に対してメソッドを呼び出したときに発生。最も遭遇頻度が高い
  • ArrayIndexOutOfBoundsException:配列の範囲外にアクセスしたときに発生
  • NumberFormatException:文字列を数値に変換するときに不正な値が渡された場合に発生
  • IllegalArgumentException:メソッドに不正な引数が渡された場合に発生

非チェック例外は「バグの結果として発生するもの」という位置づけです。catchで握り潰すより、根本原因を修正することが大切です。

現場での判断基準|どちらを投げるべきか

自分でカスタム例外を作るとき、チェック例外と非チェック例外のどちらにするか悩むことがあります。

判断基準は1つです。「呼び出し側がその例外を回復できるかどうか」で考えます。

  • 回復できる例外(例:ファイルが見つからない→別のファイルを試す)→ チェック例外
  • 回復できない例外(例:プログラムのバグ、不正な引数)→ 非チェック例外

実際の現場で感じていることを言うと、Spring Bootを使ったWebアプリ案件では非チェック例外を使うケースが圧倒的に多いです。

フレームワーク側でグローバルな例外ハンドリング(@ExceptionHandlerなど)を設けるアーキテクチャが一般的なため、チェック例外の強制処理より非チェック例外で統一する方がコードがスッキリするためです。

Zetto

使い分けの感覚がつかめると、設計の視点が一段上がります。

現場エンジニアが使う例外処理の実践テクニック

例外処理の実践テクニック

この章では、実務でよく使う例外処理のテクニックを4つ紹介します。

  • try-with-resourcesでリソース解放を自動化する
  • throwとthrowsの違いと正しい使い方
  • カスタム例外クラスの作り方と使いどころ
  • やりがちな例外処理のアンチパターン

順に見ていきましょう。

try-with-resourcesでリソース解放を自動化する

try-with-resourcesは、Java 7から追加された構文です。

ファイルやデータベース接続のように「使い終わったら必ず閉じなければならないリソース」を自動でクローズしてくれます。

AutoCloseable(自動でcloseが呼ばれるインターフェース)を実装したクラスであれば使えます。

書き方はこうです。

// tryのかっこ内でリソースを宣言する
try (FileReader reader = new FileReader("sample.txt");
     BufferedReader br = new BufferedReader(reader)) {

    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    System.out.println("ファイル読み込みエラー: " + e.getMessage());
}
// tryブロックを抜けると reader と br が自動でクローズされる

finallyブロックで手動クローズする書き方と比べると、コードの行数が減り、閉じ忘れのバグも防げます。新しいコードを書く場合は、リソースを扱う処理は基本的にtry-with-resourcesを使いましょう。

throwとthrowsの違いと正しい使い方

混乱しやすいthrowとthrowsの違いを整理します。

throw:メソッド内で例外を実際に「投げる」キーワード
throws:メソッドの宣言に書き、「この例外を呼び出し元に伝播させる」ことを示すキーワード

コード例で確認しましょう:

// throws:このメソッドはIllegalArgumentExceptionを投げる可能性があると宣言
public void setAge(int age) throws IllegalArgumentException {
    if (age < 0) {
        // throw:実際に例外オブジェクトを投げる
        throw new IllegalArgumentException("年齢は0以上を指定してください");
    }
    this.age = age;
}

非チェック例外(RuntimeExceptionのサブクラス)はthrows宣言を省略できます。ただし、明示的に書いておくとメソッドのドキュメントとして役立つことがあります。

カスタム例外クラスの作り方と使いどころ

実務では、標準の例外クラスだけでなく、プロジェクト専用のカスタム例外を作ることがよくあります。

カスタム例外を使うメリットは次の通りです。

・意図が伝わりやすい:IllegalArgumentExceptionよりInvalidUserAgeExceptionの方がコードの意味が明確になる
catchで絞り込みやすい:プロジェクト固有の例外だけを一箇所でまとめてハンドリングできる

作り方はシンプルです。

// 非チェック例外として作る場合は RuntimeException を継承する
public class InvalidUserAgeException extends RuntimeException {

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

    // エラーコードなど追加情報を持たせることもできる
    public InvalidUserAgeException(String message, Throwable cause) {
        super(message, cause);
    }
}

在庫管理Webアプリの案件では、業務ロジックのエラーを表すBusinessExceptionという親クラスを作り、そこからStockShortageException(在庫不足)などのカスタム例外を派生させる設計を使っていました。

Spring Bootの@ExceptionHandlerでまとめてエラーレスポンスを返せるので、API全体のエラー処理がシンプルに整理されます。

絶対にやってはいけない例外処理のアンチパターン

例外処理でよくやってしまうアンチパターンを3つ挙げます。

  • 例外を握り潰す:catchブロックの中が空、またはe.printStackTrace()だけで何も対処しない。エラーが起きていても気づかなくなるため、ログ出力か適切なリスロー(再度例外を投げること)が必要
  • 何でもExceptionでキャッチする:catch (Exception e)で全例外を1箇所で受け取り、同じ処理をしてしまう。原因に応じた対処ができなくなる
  • 例外を制御フローに使う:「例外が発生したらこのルートに分岐する」という書き方。例外は通常フローより処理コストが高いため、if文など通常の条件分岐で対応できる場合はそちらを使う

実務でコードレビューをしていると、この3つのどれかに当たるコードが意外と多いです。特に「例外を握り潰す」パターンはデバッグで詰まる原因になりやすいので、気をつけましょう。

実践的なテクニックを押さえたことで、日々のコーディングで判断に迷う場面が減るはずです。

Zetto

try-with-resourcesは「finallyでcloseを書き忘れてリソースリークが起きた」という実際の経験から生まれた構文です。フレームワークに慣れると隠れがちですが、仕組みを理解しているかどうかが、現場での対応力に直結します。

Javaの例外処理の演習問題

例外処理の演習問題

理解を深めるために、演習問題を3つ用意しました。

  • 演習問題1
  • 演習問題2
  • 演習問題3

演習問題1

問題

以下のコードには問題があります。問題点を指摘し、修正したコードを書いてください。

public int divide(int a, int b) {
    try {
        return a / b;
    } catch (Exception e) {
        // 何もしない
    }
    return 0;
}

解答例

public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("除数に0は指定できません");
    }
    return a / b;
}

または、例外を使うなら:

public int divide(int a, int b) {
    try {
        return a / b;
    } catch (ArithmeticException e) {
        System.err.println("ゼロ除算エラー: " + e.getMessage());
        throw new IllegalArgumentException("除数に0は指定できません", e);
    }
}

解説

元のコードには2つの問題があります。1つ目は「例外を握り潰している」点です。catchブロックが空のため、ゼロ除算が起きても呼び出し元は気づけません。

2つ目は「Exceptionで全例外をキャッチしている」点です。ArithmeticException(算術例外)のみをキャッチするか、事前にif文でゼロチェックをするのが適切な対処です。

演習問題2

問題

ファイルを読み込む処理を書いてください。条件は次の通りです。

  • try-with-resourcesを使うこと
  • IOExceptionが発生した場合はメッセージをコンソールに出力すること

解答例

import java.io.*;

public void readFile(String filePath) {
    try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (FileNotFoundException e) {
        System.err.println("ファイルが見つかりません: " + filePath);
    } catch (IOException e) {
        System.err.println("ファイル読み込みエラー: " + e.getMessage());
    }
}

解説

try-with-resourcesを使うことで、BufferedReaderとFileReaderはtryブロックを抜けると自動でクローズされます。

FileNotFoundExceptionはIOExceptionのサブクラスなので、より具体的な例外を上に書き、汎用的なIOExceptionを下に書く順番が大切です。

演習問題3

問題

以下の仕様でカスタム例外クラスを作り、それを使ったメソッドを書いてください。

  • InvalidEmailExceptionという名前の非チェック例外クラスを作る
  • validateEmail(String email)メソッドを作り、emailがnullまたは@を含まない場合にInvalidEmailExceptionを投げる

解答例

// カスタム例外クラス
public class InvalidEmailException extends RuntimeException {
    public InvalidEmailException(String message) {
        super(message);
    }
}

// バリデーションメソッド
public void validateEmail(String email) {
    if (email == null || !email.contains("@")) {
        throw new InvalidEmailException("メールアドレスの形式が正しくありません: " + email);
    }
    System.out.println("有効なメールアドレスです: " + email);
}

// 呼び出し例
public static void main(String[] args) {
    try {
        validateEmail("test@example.com"); // 正常
        validateEmail("invalid-email");    // InvalidEmailException 発生
    } catch (InvalidEmailException e) {
        System.err.println("バリデーションエラー: " + e.getMessage());
    }
}

解説

RuntimeExceptionを継承することで非チェック例外になり、throws宣言が不要になります。バリデーション系の処理では、このようにプロジェクト固有のカスタム例外を作ることでエラーの種類を明確に表現できます。

nullチェックを先に行い、containsでフォーマットチェックをするシンプルな実装で、入力検証の基本パターンを確認できます。

Mirai

演習問題むずかしかった…。

Zetto

最初は読んでわかるレベルでOKだよ。解答例を手で写経(手で入力して動かすこと)するところから始めると、実力がついていくよ。

Java例外処理をマスターして現場で通用するコードを書こう

Java例外処理マスター

この記事では、Javaの例外処理について次の内容を解説しました。

  • 例外(Exception)とエラー(Error)の違い、3種類の分類
  • try-catch-finallyの基本構文と複数例外のキャッチ方法
  • チェック例外と非チェック例外の使い分けの考え方
  • try-with-resources・throw/throws・カスタム例外などの実践テクニック
  • やりがちな例外処理のアンチパターン

例外処理は、書けるだけでなく「なぜこの書き方をするのか」を理解することが大切です。

僕自身、Java Goldを取得する過程で例外の設計思想を体系的に学び直しましたが、その後の案件でコードレビューや設計の場面でかなり役立ちました。

例外処理の書き方がわかったら、次はオブジェクト指向の設計や、Spring Bootを使った実装の仕組みも学んでいくと、現場で通用するJavaエンジニアとしての幅が広がっていきます。

Zetto

例外処理は「動くコードを書く」より「壊れにくいコードを書く」ための技術です。地味に見えますが、実務ではこの差が大きく出ます。ぜひ今日学んだことを自分のコードに取り入れてみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次