PR

Java:try-catch(例外処理)の基本を3分で解説

Java

例外処理とは、プログラムの実行中に発生するエラーや予期しない事態に対処するための仕組みのこと。例外処理を適切にプログラムに組み込むことで、プログラムがエラーで突然止まってしまうのを防いだり、エラーが発生しても適切にプログラムの実行を続けることができたりするようになります。

例えばほとんどのプログラミング言語に共通する事象として「0で割り算をしようとするとエラーが発生する」というものがあります。これを例外処理なしで実行すると、プログラムはそこで止まってしまいます。

が、しかし、例外処理を使えばこのエラーを検知して「エラーが発生しました」というメッセージを表示し、その後のプログラムを続行することができるようになります。

0で割り算をする、という以外にも自分で新たな例外を作成することで、例えば「年齢は0以上でなければならない」というルールを守らない入力があった場合に、専用のエラーメッセージを表示することができるように。

このページでは、この例外処理をJavaで実現する方法(try-catch構文)の使い方やコツ・注意点を初心者向けにわかりやすく解説します。

スポンサーリンク

Java:try-catchの基本構文

Javaではプログラム中にエラーが発生する可能性のある部分をtryブロックで囲み、エラーが発生した場合の処理をcatchブロックで定義することで例外処理を利用することができるようになります。

以下は、try-catchの基本的な使い方を示す例。

この例では、0で割り算を行い、発生したエラーをキャッチして処理しています。

public class TryCatchExample {
    public static void main(String[] args) {
        try {
            // 例外が発生する可能性のあるコード
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            // 例外が発生した場合の処理
            System.out.println("エラーが発生しました: " + e.getMessage());
        }
    }
}

// 出力結果:
// エラーが発生しました: / by zero

この例では10を0で割ろうとしていますが、これは「ArithmeticException」というエラーを引き起こします。tryブロック内でエラーが発生すると、プログラムはcatchブロックに移りArithmeticException」が発生した場合にエラーメッセージを表示する仕組み。

このように、try-catchを使うことで、エラーが発生してもプログラムを停止させず、適切なエラーメッセージを表示して処理を続行することができます。

ポイント try-catchの構文ルール

try {
    // 例外が発生する可能性のあるコード
} catch (例外の種類 例外の変数) {
    // 例外が発生した場合の処理
}
  • tryブロック: 例外が発生する可能性のあるコードを囲む。
  • catchブロック: 例外が発生した場合に実行されるコードを定義。

複数のcatchブロック

Javaでは、複数の異なる種類の例外をキャッチするために複数のcatchブロックを使用することができるため、異なる種類のエラーに対して異なる処理を行うことも可能です。

↓の例では、配列の範囲外アクセスを試みています。最初のcatchブロックが「ArrayIndexOutOfBoundsException」をキャッチし「配列のインデックスが範囲外です」というメッセージを表示します。もし他の種類の例外が発生した場合は、次のcatchブロックでキャッチされ、「その他のエラー」というメッセージを表示します。

public class MultipleCatchExample {
    public static void main(String[] args) {
        try {
            int[] numbers = {1, 2, 3};
            System.out.println(numbers[5]); // 配列の範囲外アクセス
        } catch (ArrayIndexOutOfBoundsException e) {
            // 配列の範囲外アクセスが発生した場合の処理
            System.out.println("配列のインデックスが範囲外です: " + e.getMessage());
        } catch (Exception e) {
            // その他の例外が発生した場合の処理
            System.out.println("その他のエラー: " + e.getMessage());
        }
    }
}

// 出力結果:
// 配列のインデックスが範囲外です: Index 5 out of bounds for length 3

このように、複数のcatchブロックを使うことで、異なる種類の例外に対して適切なエラーハンドリングを行うことができます。

ポイント 複数のcatchブロックを利用するパターンの構文ルール

try {
    // 例外が発生する可能性のあるコード
} catch (具体的な例外1 例外の変数) {
    // 具体的な例外1が発生した場合の処理
} catch (具体的な例外2 例外の変数) {
    // 具体的な例外2が発生した場合の処理
} catch (一般的な例外 例外の変数) {
    // その他の例外が発生した場合の処理
}
  1. 複数のcatchブロック: tryブロック内で発生する可能性のある異なる種類の例外ごとにcatchブロックを定義する。
  2. 例外の順序: より具体的な例外から順に記述し、最後に一般的な例外をキャッチするcatchブロックを記述する。

catch文では、継承元のクラスもキャッチ可能 です。たとえば、RuntimeExceptionException を継承しているので、以下のように書いた場合:

try {
    throw new RuntimeException("例外発生");
} catch (Exception e) {
    System.out.println("Exceptionでキャッチ: " + e.getMessage());
}

このコードは "Exceptionでキャッチ: 例外発生" と出力され、RuntimeExceptionException でキャッチしています。

注意点としては、catchの順番です。たとえば次のように書くとコンパイルエラーになります。

try {
    // 何らかの処理
} catch (Exception e) {
    // 先にExceptionをキャッチ
} catch (RuntimeException e) {
    // これは到達不能なのでエラー
}

try-catch-finally

Javaの例外処理では、finallyブロックを使用して、例外の発生有無にかかわらず必ず実行されるコードを定義することができます。リソースの解放や後処理など、どんな状況でも実行しておきたい処理を記述するために用いられます。

以下は、finallyブロックの基本的な使い方を示す例。この例では、0で割り算を行い、発生したエラーをキャッチした後、必ず実行される処理をfinallyブロックで定義しています。

public class FinallyExample {
    public static void main(String[] args) {
        try {
            // 例外が発生する可能性のあるコード
            int result = 10 / 0;
        } catch (ArithmeticException e) {
            // 例外が発生した場合の処理
            System.out.println("エラーが発生しました: " + e.getMessage());
        } finally {
            // 必ず実行される処理
            System.out.println("このコードは必ず実行されます。");
        }
    }
}

// 出力結果:
// エラーが発生しました: / by zero
// このコードは必ず実行されます。

ポイント finallyブロックの使い方

try {
    // 例外が発生する可能性のあるコード
} catch (例外の種類 例外の変数) {
    // 例外が発生した場合の処理
} finally {
    // 必ず実行される処理
}
  1. tryブロック: エラーが発生する可能性のあるコードを囲む。
  2. catchブロック: エラーが発生した場合に実行されるコードを定義する。
  3. finallyブロック: 例外の有無にかかわらず必ず実行されるコードを定義する。

ポイント 上記は①②③の順に記述しないといけません。仮に以下のように順番を変更するとコンパイルエラーが発生します。

//以下は順番が不正なのでコンパイルエラーが発生する

try {
    // 例外が発生する可能性のあるコード
} finally {
    // 必ず実行される処理
} catch (例外の種類 例外の変数) {
    // 例外が発生した場合の処理
}

ポイント try-catch-finally構文では、複数記述できるのはcatchのみです(tryとfinallyは1つだけ)。→異なる種類の例外(例:IOExceptionNullPointerException)を個別に処理できるようにするためです。

public class TryCatchExample {
    public static void main(String[] args) {
        try {
            int result = 10 / 0; // 例外発生(ArithmeticException)
        } catch (ArithmeticException e) {
            System.out.println("算術エラーが発生しました: " + e.getMessage());
        } catch (NullPointerException e) {
            System.out.println("ヌルポ発生!");
        } finally {
            System.out.println("処理終了。"); // finallyは1つしか定義できない
        }
    }
}
Q
catch は結局何を受け取っているのか?
A

catch ブロックは「投げられた例外オブジェクト」を受け取っている、と捉えると分かりやすいです。

  • 「throw」=「特別な情報を投げる」
    例外が起きると、Javaは「問題が起きた!」というメッセージや原因を含む“例外オブジェクト”を作って、呼び出し元方向に向かって投げます(throw new SomeException(...))。
  • 「catch」=「投げられたものを受け取って処理する」
    その“例外オブジェクト”を受け取りつつ、エラーに対して何らかの対応(ログを出す、再試行する、ユーザーにメッセージを表示するなど)をするのが catch ブロックです。

少し比喩すると、

  1. メソッドが処理中に「ヤバい!」と思ったら「エラーが起きたぞ!」という手紙を投げる(throw)。
  2. 上位のどこかのメソッドが「その手紙は自分が受け取るよ」と言って、それを受け取り(catch)、中に書かれたエラー情報を読み取って何とか対処する。

こう考えると、「catch ブロックはいったい何をキャッチしているのか?」は、「エラーの詳細が詰まった“特別なオブジェクト”を受け取っている」ということになります。

カスタム例外

Javaでは標準で提供される例外クラス(例:NullPointerExceptionArrayIndexOutOfBoundsException)を使ってエラーを処理することができますが、同時に特定のアプリケーションに合わせて独自の例外を作成することも可能です。これを「カスタム例外」と呼びます。カスタム例外を使うことで、特定のエラー状況に対してよりわかりやすく、意味のあるエラーメッセージを提供することができます。

カスタム例外は、Javaの標準例外クラスを継承して作成する独自の例外クラスです。これにより、特定のビジネスロジックやアプリケーションの要件に応じたエラー処理を行うことができます。例えば、年齢が負の値になってはいけないというルールを守らない入力があった場合に、専用のエラーメッセージを提供するためにカスタム例外を作成します。

参考 Java「クラス」とは? / Java「クラスの継承」とは?

カスタム例外の作成

  1. カスタム例外クラスの定義
    • 標準の例外クラス(例:ExceptionRuntimeException)を継承して新しいクラスを作成する
  2. カスタム例外の使用
    • 必要に応じて、カスタム例外をスロー(throw)し、catchブロックで処理。

早速、カスタム例外を作成してみます。

まず、カスタム例外クラスを定義します。このクラスはExceptionクラスを継承します。

// カスタム例外クラスの定義
public class CustomException extends Exception {
    public CustomException(String message) {
        super(message);
    }
}

次に、カスタム例外をスローし、catchブロックで処理します。この例では、入力された値が0未満の場合にカスタム例外をスローします。

public class CustomExceptionExample {
    public static void main(String[] args) {
        try {
            checkValue(-1);
        } catch (CustomException e) {
            // カスタム例外が発生した場合の処理
            System.out.println("カスタム例外が発生しました: " + e.getMessage());
        }
    }

    // 値をチェックし、条件に合わない場合にカスタム例外をスローするメソッド
    public static void checkValue(int value) throws CustomException {
        if (value < 0) {
            throw new CustomException("値は0以上でなければなりません。");
        }
    }
}

// 出力結果:
// カスタム例外が発生しました: 値は0以上でなければなりません。

checkValueメソッドが負の値を受け取ると、CustomExceptionをスローするというコード。mainメソッド内でこのメソッドを呼び出し、カスタム例外がスローされた場合はcatchブロックで処理します。このように、カスタム例外を使用することで、特定のエラー状況に対して意味のあるエラーメッセージを提供し、プログラムの可読性とメンテナンス性を向上させることができます。

ポイント throwの構文ルール

throw new 例外クラス名("エラーメッセージ");
  1. throw キーワード: 例外オブジェクトをスローするために使用します。
  2. 例外オブジェクト: newキーワードを使用して例外クラスのインスタンスを作成します。

以下は、throwキーワードを使用して標準のIllegalArgumentExceptionをスローする例。

public class ThrowExample {
    public static void main(String[] args) {
        checkValue(-1);
    }

    public static void checkValue(int value) {
        if (value < 0) {
            throw new IllegalArgumentException("値は0以上でなければなりません。");
        }
        System.out.println("値は正しいです: " + value);
    }
}

// 出力結果:
// Exception in thread "main" java.lang.IllegalArgumentException: 値は0以上でなければなりません。
//     at ThrowExample.checkValue(ThrowExample.java:8)
//     at ThrowExample.main(ThrowExample.java:4)

throw の構文の意味

throw new NullPointerException("オブジェクトがnullです");

↑のコードの意味をより詳しく分解すると

  1. new を使って「オブジェクトがnullです」というメッセージを持つ NullPointerException の新しいインスタンス(例外オブジェクト)を作ります。
  2. throw によって、その例外オブジェクトをすぐに投げ(発生させ)、プログラムの実行が中断され、適切な catch ブロックで処理されるか、なければエラーとしてプログラムを終了させる。

という意味になります。ここで、①のnew は例外クラスをインスタンス化しているだけなので、実は以下のように記述しても意味は全く同じです。

NullPointerException a = new NullPointerException("オブジェクトがnullです");
throw a;

例外処理をより詳しく、実践的に理解する

ここからはこれまでに解説した基本を踏まえ、より例外処理を実践的に使いこなせるようにその動作原理やJavaの裏側についてより詳細に解説していきます。

Javaにおける例外処理は、プログラムの実行中に何らかの異常事態(エラー状況)が発生した場合に、その処理を呼び出し元など上位のコードに通知し、適切に対処できるようにする仕組みでした。
例外を使うことで、エラー発生時に戻り値などで状態を示す代わりに、「本来の処理フローを中断し、対処が可能な場所までジャンプする」形で流れを切り替えることができます。

整理:Javaの例外処理の基本的な流れ

  1. 例外が発生(throw)する
    • throw new SomeException("エラー理由"); のように明示的に投げられるケース。
    • 読み込み処理中にファイルが見つからない、配列の範囲外にアクセスした、などシステム側が検知して自動的に発生させるケース。
  2. スタックの巻き戻し(stack unwinding)が行われる
    • 例外が発生すると、Java仮想マシン(JVM)は呼び出しスタックを下にたどっていき、対応する catch 節を探す。
    • 見つかるまでメソッド呼び出しをどんどん終了(巻き戻し)していく。
  3. 対応する catch 節で処理される
    • スタック上で「指定された型またはそのサブクラスの例外を捕捉できる」catch が存在する場合、その箇所に処理が移る。
    • 例:catch (IOException e) { ... } であれば、IOException とそのサブクラスを捕捉する。
  4. 例外が捕捉できない場合
    • どのメソッドを遡っても該当の catch 節がないなら、最終的に JVM が受け取ってエラーとしてプログラムが終了する。
  5. 任意で finally 節が実行される
    • 例外の有無にかかわらず、リソース解放や終了処理は finally 節に書いておくことで必ず実行できる。

スタックの巻き戻し

たとえば以下のようにメソッドが呼び出されていたとします。

main() → methodA() → methodB() → methodC()
  1. methodC() 内で例外が発生
    throw new SomeException("何らかのエラー"); によって例外オブジェクトが作られ投げられる。
  2. methodC() 内に対応する catch がなければメソッドを終了(スタックフレーム破棄)
    • methodC() のローカル変数や状態は失われ、呼び出し元の methodB() に戻る。
  3. methodB() で対応する catch を探す
    • なければ methodB() も終了(破棄)し、さらに呼び出し元の methodA() に戻る。
  4. methodA() でも見つからなければ終了
    • 同様に破棄して、最終的に main() メソッドに行き着く。
  5. すべての呼び出し元に catch がなければ、JVM が例外を受け取ってアプリケーションを終了する
    • スタックトレース(どのメソッドで例外が発生したか)が標準エラーに出力される。

このように、スタック上の各メソッドフレームを「巻き戻し」しながら、例外を補足できるブロックを探していく仕組みがスタックの巻き戻しです。

スタックを巻き戻す途中、メソッド内に finally ブロックがあれば必ず実行されます。
try { ... } catch(...) { ... } finally { ... } と書かれている場合、例外の有無にかかわらず必ず finally が呼ばれます。したがって、「巻き戻し中に使ったリソースを解放する」「ログを出力する」といった後処理を正しく行えます。

例外クラス

Throwable を頂点とした階層構造

Java の例外は、クラス階層として見ると以下のような構造になっています。

java.lang.Object
  └─ java.lang.Throwable
       ├─ java.lang.Error
       └─ java.lang.Exception
            └─ java.lang.RuntimeException
                 └─ ...

Throwable

  • Java で「投げられる(throw できる)もの」を表す最上位のクラスです。
  • Error Exception の2系統に大きく分かれます。

Error

  • JVM の致命的なエラー(例:OutOfMemoryError StackOverflowError)が含まれます。
  • 通常のアプリケーションでは捕捉しても対処が難しいため、ほとんどの場合は積極的に扱いません。

Exception

  • アプリケーションで処理可能な問題が含まれます。
  • IOExceptionSQLExceptionClassNotFoundException などが代表例です。

RuntimeException

  • Exception のサブクラスですが、RuntimeException やそのサブクラスは「実行時例外」として扱われます(例:NullPointerExceptionArrayIndexOutOfBoundsExceptionIllegalArgumentException など)。
  • チェック例外(後述)と異なり、コンパイラが try-catchthrows 宣言を強要しない性質を持ちます。

チェック例外と実行時例外(unchecked例外)の違い

  • チェック例外(Checked Exception)
    • Exception のサブクラスのうち、RuntimeException 以外のもの。
    • メソッド呼び出し側が例外処理を「必ず」意識しなければならないという言語仕様がある。
    • 例:IOExceptionSQLExceptionClassNotFoundException など。
  • 実行時例外(Unchecked Exception / RuntimeException)
    • メソッド呼び出し側が try-catch を書かずに放置してもコンパイルエラーにはならない。
    • 主にプログラムのロジック上のミス(NullPointerExceptionIndexOutOfBoundsException)などに使われる。
    • 例:NullPointerExceptionIllegalArgumentExceptionNumberFormatException など。

Javaでは、プログラム中で起こる「問題」をオブジェクトとして扱います。その元となるのが Throwable クラスです。ここから「例外」と「エラー」という大きく2つのグループに分かれます。

ポイント Throwable:全ての問題の元

  • Throwable
    Javaのすべての「問題」はこのクラスか、そのサブクラスのインスタンスとして表されます。つまり、何か問題が起こると、Throwable(またはその派生クラス)のオブジェクトが生成されます。

ポイント Error:システムの大問題

  • Error
    • システムそのものや環境に起因する重大な問題を表します。
    • 例としては、メモリ不足(OutOfMemoryError)やスタックオーバーフロー(StackOverflowError)があります。
    • 通常、Errorはプログラム側で捕まえて対処するものではなく、発生した場合はプログラムの実行を続けるのが難しいため、あまり触るべきではありません。

ポイント Exception:プログラムが対処できる問題

  • Exception
    プログラムの動作中に発生する「例外的な状況」を表し、これらは開発者が捕まえて適切に対処できるものです。Exceptionはさらに次の2種類に分けられます。
    • チェック例外(Checked Exception)
      • コンパイル時に「この例外が発生するかもしれないから、対処しなさい」と指摘される例外です。
      • 例:ファイル操作で使われるIOExceptionなど
      • メソッド内で発生が予測される場合は、try-catchで捕まえるか、メソッド宣言にthrowsを付ける必要があります。
    • 非チェック例外(Unchecked Exception)
      • プログラムのバグや論理的なミスにより発生する例外で、コンパイル時に対処を強制されません。
      • 例:NullPointerExceptionArithmeticExceptionなど
      • 主にRuntimeExceptionのサブクラスとして実装されています。

ポイント ユーザが作る独自の例外クラス

自分のプログラムで、特定の状況に合わせた例外を作りたい場合、以下のどちらかを継承して作成します。

  • チェック例外として作成する場合
    Exception を継承します。
public class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}

この場合、例外を発生させるメソッドでは、呼び出し元に対して例外処理を強制されます。

  • 非チェック例外として作成する場合
    RuntimeException を継承します。
public class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}

// この場合、呼び出し元は例外処理を必ずしも記述する必要はありません。
  • Throwable が全ての問題のルートクラスです。
  • Error はシステムレベルの深刻な問題で、通常は捕まえて処理しません。
  • Exception はプログラム内で予測可能な問題を扱い、チェック例外と非チェック例外に分かれます。
  • ユーザは、自分の必要に合わせて Exception または RuntimeException を継承することで、独自の例外クラスを作ることができます。

このような仕組みにより、Javaはプログラムのエラー発生時に適切な対処を促すとともに、プログラムの信頼性を高めています。

例外クラスの役割

  1. 情報を持つことができる
    • 例外クラスはオブジェクトなので、コンストラクタでメッセージやエラーコードなどの詳細情報を持たせることが可能です。
    • 例外を捕捉した側で e.getMessage() のようにしてメッセージを参照し、ログ出力やユーザへの通知に活用できます。
  2. 継承で種類を表現できる
    • 独自の例外クラスを定義することで「自分のアプリケーションで起こりうる特有のエラー」を表現できます。
    • サブクラスを作ることでエラーの粒度を細分化し、よりきめ細かくハンドリング可能にします。
  3. ポリモーフィズムの適用
    • スーパークラス型(たとえば Exception)でキャッチすると、すべてのサブクラスを一括で捕捉できます。
    • 特定のサブクラスだけをキャッチするようにすると、個別のエラー対処を行うこともできます。

例外クラスの定義例

実際に独自の例外クラスを定義する例を見てみましょう。たとえば「アプリで定義した ID が重複した場合」にスローする独自例外 DuplicateIdException をチェック例外として作る場合です。

public class DuplicateIdException extends Exception {
    public DuplicateIdException() {
        super();
    }

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

    public DuplicateIdException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • Exception を継承したことで、「チェック例外」として扱われます。
  • コンストラクタのオーバーロードにより、メッセージや原因(cause)を自由に渡せるようにしておくのが一般的です。
  • この例外を投げるメソッドは、throws DuplicateIdException と宣言する必要があり、呼び出し側は必ず try-catch か、さらに throws の委譲をする必要があります。

JVM内部での例外の動作原理(スタックアンワインド)

Java のメソッド呼び出しは、JVM 上では「呼び出しスタック」という形で管理されています。
例外が投げられると、以下のようにスタックを遡っていきます。

  1. 例外オブジェクトが生成される
    • throw キーワードや内部エラー検知で new SomeException(...) が呼ばれることで例外インスタンスが生成されます。
  2. 現在のメソッドが対応する catch を持っているか確認
    • try-catch ブロックがあるか、かつキャッチできる型かどうかをチェックします。
  3. なければ呼び出し元に遡る
    • 現在のメソッドを抜けて、呼び出し元メソッドの catch があるかチェックします。
    • これを再帰的に繰り返します。
  4. 最終的に main メソッド(プログラムの起点)まで到達
    • もし最後まで捕捉されなければ JVM の既定のエラーハンドラが受け取り、スタックトレース(どこでエラーが起きたかの情報)を表示してプログラムを終了します。

まとめ Javaの例外

  • Java の例外は「オブジェクトとして扱われる」
    • 例外にはメッセージや原因の例外(cause)など情報を保持させられる。
  • 例外クラスは Throwable を継承し、主に Exception または RuntimeException をサブクラスとして利用する
    • Error は通常のアプリケーションでは扱わない(致命的なエラー向け)。
  • Java のコンパイル時にチェックされるかどうかで、チェック例外と実行時例外に分かれる
    • チェック例外は throws 宣言や try-catch を必ず書く必要がある。
  • スタックの巻き戻しによって上位メソッドへ伝播し、適切な catch 節が見つかるか JVM が最終的に処理を打ち切る
    • これが例外処理機構の核心部分。
タイトルとURLをコピーしました