Javaにおける例外処理は、プログラムの信頼性と安全性を高めるために欠かせない重要な機能です。
Javaの例外処理は「try-catch-finally」ブロックによる捕捉・「throws」によるメソッドからの例外宣言・「throw」による例外の明示的な発生という3つの主要な仕組みで構成されています。
さらに、Javaには検査例外(checked exception)と非検査例外(unchecked exception)という2種類の例外分類があり、それぞれに適した扱い方があります。
本記事では、例外処理のJavaでの実装方法、基本構文と書き方、try-catch-finally・throws・throw・checked/unchecked例外・Exceptionクラスなどについて詳しく解説していきます。
Java入門者の方から、より良いコード品質を目指す中級者の方まで、実践的な知識を習得していただける内容です。
JavaのException(例外)クラス階層を理解することが実装の出発点
それではまず、Javaの例外クラスの階層構造と、それぞれの違いについて解説していきます。
Javaでは例外はすべてクラスとして定義されており、Throwableクラスを頂点とした継承ツリーを形成しています。
Throwableの直接の子クラスはErrorとExceptionの2つです。
Errorはシステムレベルの回復不可能な問題(OutOfMemoryError・StackOverflowErrorなど)を表し、通常はアプリケーションでキャッチするべきではありません。
Exceptionはアプリケーションが処理可能な例外の基底クラスであり、検査例外と非検査例外に分かれます。
Throwable
├── Error(回復不可能)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception(回復可能)
├── IOException(検査例外)
├── SQLException(検査例外)
└── RuntimeException(非検査例外の親)
├── NullPointerException
├── ArithmeticException
└── ArrayIndexOutOfBoundsException
検査例外(checked exception)とは
検査例外とは、コンパイル時に処理が強制される例外です。
ExceptionクラスのサブクラスのうちRuntimeExceptionを継承しないものが検査例外に該当します。
代表例:IOException・FileNotFoundException・ClassNotFoundException・SQLException
これらの例外はメソッド内でtry-catchで処理するか、throws宣言でメソッドの呼び出し元に伝播させるかのどちらかが必須です。
どちらも行わないとコンパイルエラーになります。
非検査例外(unchecked exception)とは
非検査例外とはRuntimeExceptionを継承する例外であり、コンパイル時のチェックが不要です。
代表例:NullPointerException・ArrayIndexOutOfBoundsException・ArithmeticException・ClassCastException・IllegalArgumentException
非検査例外は通常、プログラムのバグ(nullチェック漏れや配列の境界外アクセスなど)を表すため、catch文で無理に処理するよりもバグを修正することが根本的な対処法です。
Javaのtry-catch-finallyの基本構文
Javaでの例外処理の基本構文は以下の通りです。
try {
// 例外が発生する可能性のある処理
FileReader fr = new FileReader(“data.txt”);
} catch (FileNotFoundException e) {
// FileNotFoundExceptionが発生したときの処理
System.out.println(“ファイルが見つかりません: ” + e.getMessage());
} catch (IOException e) {
// IOExceptionが発生したときの処理
System.out.println(“入出力エラー: ” + e.getMessage());
} finally {
// 必ず実行される処理
System.out.println(“処理終了”);
}
複数のcatchブロックを書く場合は、より具体的(下位)の例外クラスを先に書き、より抽象的(上位)の例外クラスを後に書く必要があります。
逆にすると、コンパイルエラーまたは下位例外が捕捉されない問題が発生します。
throwsとthrowの使い方と違い
続いては、Javaの例外処理におけるthrowsとthrowの役割と正しい使い分けを確認していきます。
throwsとthrowは似た名前ですが、全く異なる役割を持っています。
throwsによる例外の宣言
throwsはメソッドのシグネチャ(定義部分)に記述し、そのメソッドが特定の検査例外を発生させる可能性があることを呼び出し元に知らせる宣言です。
public void readFile(String path) throws IOException {
FileReader fr = new FileReader(path);
// ファイル読み込み処理
}
このメソッドを呼び出す側は、IOExceptionをtry-catchで処理するか、さらに上位に伝播させる義務があります。
複数の例外を宣言する場合はカンマで区切ります(例:throws IOException, SQLException)。
throwによる例外の明示的な発生
throwは実行時に例外を明示的に発生させるキーワードです。
条件チェックなどでプログラム的に例外を発生させたい場合に使います。
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException(“年齢が無効な値です: ” + age);
}
this.age = age;
}
throwで発生させる例外オブジェクトはnewでインスタンスを作成する必要があります。
| キーワード | 用途 | 記述場所 |
|---|---|---|
| throw | 例外を発生させる | メソッド本体内 |
| throws | 例外を宣言する | メソッドのシグネチャ |
カスタム例外クラスの実装
業務ロジックに特化したカスタム例外クラスを作成することで、例外の意味が明確になります。
// 検査例外として定義する場合
public class InsufficientBalanceException extends Exception {
private final double shortageAmount;
public InsufficientBalanceException(double shortageAmount) {
super(“残高が” + shortageAmount + “円不足しています”);
this.shortageAmount = shortageAmount;
}
public double getShortageAmount() {
return shortageAmount;
}
}
非検査例外として定義する場合はRuntimeExceptionを継承します。
どちらを選ぶかはその例外が「呼び出し元に処理を強制すべきか否か」という設計判断によります。
try-with-resourcesと複数例外のキャッチ
続いては、Java 7以降で導入されたtry-with-resources構文と、複数例外を効率的に処理する方法を確認していきます。
これらの機能を活用することで、より簡潔で安全なコードが書けるようになります。
try-with-resourcesの仕組みと利点
Java 7から導入されたtry-with-resources構文は、AutoCloseableインターフェースを実装したリソースを自動的にクローズする機能です。
try (BufferedReader br = new BufferedReader(new FileReader(“data.txt”)); Connection conn = DriverManager.getConnection(url)) {
// リソースを使った処理
String line = br.readLine();
} catch (IOException e) {
System.out.println(“エラー: ” + e.getMessage());
}
// br と conn は自動的にclose()が呼ばれます
tryブロックを抜けると(正常終了・例外発生どちらでも)リソースのclose()が自動的に呼ばれるため、リソースリーク(解放忘れ)を防げます。
try-with-resourcesはJavaのリソース管理のベストプラクティスであり、ファイル・DB接続・ネットワークソケットを扱う際には積極的に使用すべきです。
マルチキャッチ(|による複数例外の一括処理)
Java 7から複数の例外を一つのcatchブロックでまとめて処理する「マルチキャッチ」構文が使えるようになりました。
try {
someMethod();
} catch (IOException | SQLException e) {
// IOExceptionまたはSQLExceptionをまとめて処理
System.out.println(“エラー発生: ” + e.getMessage());
}
同じ処理でよい複数の例外をマルチキャッチでまとめることでコードが簡潔になります。
ただし、例外ごとに異なる処理が必要な場合は個別のcatchブロックに分けましょう。
例外チェーンと原因の記録
例外処理では、低レベルの例外を高レベルの例外でラップして再スローする「例外チェーン」というパターンが重要です。
try {
// DB操作
} catch (SQLException e) {
throw new DataAccessException(“データ取得中にエラーが発生しました”, e);
}
コンストラクタの第2引数に元の例外(原因)を渡すことで、スタックトレースに元のエラー情報が保持され、デバッグが容易になります。
これにより技術的な詳細(SQLException)を上位層に漏らさずに、業務的な意味のある例外(DataAccessException)に変換することができます。
まとめ
本記事では、例外処理のJavaでの実装方法、try-catch-finally・throws・throw・checked/unchecked例外・Exceptionクラスなどについて解説しました。
JavaはExceptionクラスの継承ツリーに基づいた型安全な例外処理システムを持っており、検査例外と非検査例外の使い分けが設計の核心です。
try-with-resourcesやマルチキャッチ・例外チェーンなどのモダンな機能を活用することで、より安全で保守性の高いJavaコードを書くことができます。
カスタム例外クラスの定義と、throwsによる明示的な例外宣言を組み合わせることで、API利用者に優しい設計が実現できるでしょう。
Javaの例外処理をしっかりとマスターすることは、エンタープライズ品質のアプリケーション開発に向けた確固たる一歩となります。