はじめに

プログラムの実行中には、ファイルが見つからない、ネットワーク接続が切断された、不正な入力が渡されたなど、さまざまな想定外の事態が発生する可能性があります。このような状況を適切に処理しないと、プログラムが突然クラッシュし、ユーザーに不便を与えることになります。

Javaでは、このような異常事態を例外(Exception) として捉え、専用の構文で処理する仕組みが用意されています。この記事では、Javaの例外処理の基本から応用まで、段階的に解説します。

例外とは何か

例外(Exception)とは、プログラムの実行中に発生する異常なイベントのことです。例外が発生すると、通常の処理フローが中断され、例外処理のメカニズムに制御が移ります。

例外が発生する典型的なケース

以下は、Javaプログラムで例外が発生する典型的なケースです。

状況 発生する例外
配列の範囲外にアクセス ArrayIndexOutOfBoundsException
nullオブジェクトのメソッドを呼び出し NullPointerException
数値を0で割る ArithmeticException
存在しないファイルを開く FileNotFoundException
不正な形式の文字列を数値に変換 NumberFormatException

例外が発生するとどうなるか

例外が発生すると、その時点で処理が中断され、例外に関する情報(スタックトレース)がコンソールに出力されます。

1
2
3
4
5
6
7
public class ExceptionDemo {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3};
        System.out.println(numbers[5]); // 範囲外アクセス
        System.out.println("この行は実行されません");
    }
}

実行結果:

1
2
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3
    at ExceptionDemo.main(ExceptionDemo.java:4)

例外を適切に処理しないと、プログラムはクラッシュし、後続の処理は実行されません。

Javaの例外階層

Javaの例外は、java.lang.Throwableクラスを頂点とした階層構造を持っています。

classDiagram
    class Throwable {
        +String getMessage()
        +void printStackTrace()
        +Throwable getCause()
    }
    class Error {
        <<system error>>
    }
    class Exception {
        <<recoverable>>
    }
    class RuntimeException {
        <<unchecked>>
    }
    class IOException {
        <<checked>>
    }
    class SQLException {
        <<checked>>
    }
    class NullPointerException {
        <<unchecked>>
    }
    class IllegalArgumentException {
        <<unchecked>>
    }
    
    Throwable <|-- Error
    Throwable <|-- Exception
    Exception <|-- RuntimeException
    Exception <|-- IOException
    Exception <|-- SQLException
    RuntimeException <|-- NullPointerException
    RuntimeException <|-- IllegalArgumentException

ThrowableとErrorとExceptionの違い

クラス 説明 対処方法
Throwable すべての例外とエラーの基底クラス 直接使用することは少ない
Error JVMレベルの深刻な問題 通常はキャッチしない
Exception プログラムで回復可能な問題 適切にキャッチして処理

ErrorOutOfMemoryErrorStackOverflowErrorなど、アプリケーションで回復が困難な問題を表します。一方、Exceptionはプログラムで適切に処理できる問題を表します。

検査例外と非検査例外

Javaの例外は、検査例外(Checked Exception)非検査例外(Unchecked Exception) の2種類に分類されます。この分類はJava特有の重要な概念です。

検査例外(Checked Exception)

検査例外は、コンパイル時にチェックされる例外です。メソッドが検査例外をスローする可能性がある場合、呼び出し元で必ず処理するか、さらに上位に伝播させる必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import java.io.FileReader;
import java.io.IOException;

public class CheckedExceptionDemo {
    public static void main(String[] args) {
        // コンパイルエラー: 処理されない例外 FileNotFoundException
        // FileReader reader = new FileReader("test.txt");
        
        // 正しい方法1: try-catchで処理
        try {
            FileReader reader = new FileReader("test.txt");
        } catch (IOException e) {
            System.out.println("ファイルを開けませんでした");
        }
    }
}

代表的な検査例外:

例外クラス 発生状況
IOException 入出力処理の失敗
SQLException データベース操作の失敗
ClassNotFoundException クラスが見つからない
InterruptedException スレッドの割り込み

非検査例外(Unchecked Exception)

非検査例外は、RuntimeExceptionとそのサブクラスです。コンパイル時のチェックが不要で、try-catchで囲まなくてもコンパイルエラーになりません。

1
2
3
4
5
6
7
public class UncheckedExceptionDemo {
    public static void main(String[] args) {
        String text = null;
        // コンパイルは通るが、実行時にNullPointerExceptionが発生
        System.out.println(text.length());
    }
}

代表的な非検査例外:

例外クラス 発生状況
NullPointerException nullオブジェクトへのアクセス
ArrayIndexOutOfBoundsException 配列の範囲外アクセス
IllegalArgumentException 不正な引数
NumberFormatException 数値変換の失敗
ArithmeticException 不正な算術演算

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

種類 使用場面
検査例外 呼び出し元で回復可能な外部要因のエラー ファイルが存在しない、ネットワーク障害
非検査例外 プログラミングミスによるバグ null参照、範囲外アクセス

try-catch-finally文

例外を処理するための基本構文がtry-catch-finally文です。

基本構文

1
2
3
4
5
6
7
try {
    // 例外が発生する可能性のある処理
} catch (例外クラス 変数名) {
    // 例外発生時の処理
} finally {
    // 必ず実行される処理(省略可能)
}

try-catchの基本

tryブロック内で例外が発生すると、即座にcatchブロックに制御が移ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class TryCatchDemo {
    public static void main(String[] args) {
        try {
            int result = 10 / 0; // ArithmeticException発生
            System.out.println("結果: " + result); // 実行されない
        } catch (ArithmeticException e) {
            System.out.println("0で割ることはできません");
            System.out.println("エラーメッセージ: " + e.getMessage());
        }
        
        System.out.println("プログラムは正常に続行します");
    }
}

実行結果:

1
2
3
0で割ることはできません
エラーメッセージ: / by zero
プログラムは正常に続行します

例外オブジェクトのメソッド

キャッチした例外オブジェクトには、エラー情報を取得するためのメソッドが用意されています。

メソッド 説明
getMessage() 例外メッセージを取得
printStackTrace() スタックトレースを出力
getCause() 原因となった例外を取得
getClass().getName() 例外クラス名を取得
1
2
3
4
5
6
7
8
9
try {
    int[] arr = new int[3];
    arr[10] = 100;
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("例外クラス: " + e.getClass().getName());
    System.out.println("メッセージ: " + e.getMessage());
    System.out.println("スタックトレース:");
    e.printStackTrace();
}

複数のcatchブロック

1つのtryブロックに対して、複数のcatchブロックを記述できます。より具体的な例外クラスを先に、より一般的な例外クラスを後に記述します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class MultipleCatchDemo {
    public static void main(String[] args) {
        try {
            String input = "abc";
            int number = Integer.parseInt(input); // NumberFormatException
            int result = 100 / number;
        } catch (NumberFormatException e) {
            System.out.println("数値形式が不正です: " + e.getMessage());
        } catch (ArithmeticException e) {
            System.out.println("算術エラー: " + e.getMessage());
        } catch (Exception e) {
            // その他すべての例外をキャッチ
            System.out.println("予期しないエラー: " + e.getMessage());
        }
    }
}

マルチキャッチ(Java 7以降)

Java 7以降では、複数の例外を1つのcatchブロックでまとめて処理できます。

1
2
3
4
5
6
try {
    // 何らかの処理
} catch (IOException | SQLException e) {
    // IOExceptionとSQLExceptionを同じ方法で処理
    System.out.println("入出力またはデータベースエラー: " + e.getMessage());
}

マルチキャッチを使用する際の注意点:

  • 列挙する例外クラス間に継承関係があってはいけません
  • キャッチした変数は暗黙的にfinalになります

finallyブロック

finallyブロックは、例外の発生有無にかかわらず必ず実行される処理を記述します。リソースの解放処理に使用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.FileReader;
import java.io.IOException;

public class FinallyDemo {
    public static void main(String[] args) {
        FileReader reader = null;
        try {
            reader = new FileReader("test.txt");
            // ファイル読み込み処理
            System.out.println("ファイルを読み込みました");
        } catch (IOException e) {
            System.out.println("ファイルの読み込みに失敗しました");
        } finally {
            // 例外発生の有無に関わらず必ず実行
            if (reader != null) {
                try {
                    reader.close();
                    System.out.println("リソースを解放しました");
                } catch (IOException e) {
                    System.out.println("リソースの解放に失敗しました");
                }
            }
        }
    }
}

finallyが実行されないケース

以下の場合、finallyブロックは実行されません:

  • System.exit()が呼び出された場合
  • JVMがクラッシュした場合
  • 無限ループやデッドロックで処理が終わらない場合

try-with-resources(Java 7以降)

finallyでリソースを解放するコードは冗長になりがちです。Java 7で導入されたtry-with-resources文を使用すると、リソースの自動解放を簡潔に記述できます。

基本構文

1
2
3
4
5
6
try (リソースの宣言) {
    // リソースを使用した処理
} catch (例外クラス e) {
    // 例外処理
}
// tryブロックを抜けると自動的にリソースがクローズされる

AutoCloseableインターフェース

try-with-resourcesで使用できるリソースは、AutoCloseableインターフェースを実装している必要があります。

classDiagram
    class AutoCloseable {
        <<interface>>
        +void close()
    }
    class Closeable {
        <<interface>>
        +void close()
    }
    class FileReader
    class FileWriter
    class BufferedReader
    class Connection
    
    AutoCloseable <|-- Closeable
    Closeable <|.. FileReader
    Closeable <|.. FileWriter
    Closeable <|.. BufferedReader
    AutoCloseable <|.. Connection

実践例

従来のfinallyを使った書き方とtry-with-resourcesを比較してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesDemo {
    
    // 従来の書き方(冗長)
    public static void readFileOldStyle(String path) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(path));
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("読み込みエラー: " + e.getMessage());
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.out.println("クローズエラー: " + e.getMessage());
                }
            }
        }
    }
    
    // try-with-resourcesを使った書き方(簡潔)
    public static void readFileNewStyle(String path) {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.out.println("読み込みエラー: " + e.getMessage());
        }
        // readerは自動的にクローズされる
    }
}

複数リソースの管理

複数のリソースをセミコロンで区切って宣言できます。リソースは宣言と逆順でクローズされます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class MultipleResourcesDemo {
    public static void copyFile(String source, String dest) {
        try (
            FileReader reader = new FileReader(source);
            FileWriter writer = new FileWriter(dest)
        ) {
            int ch;
            while ((ch = reader.read()) != -1) {
                writer.write(ch);
            }
            System.out.println("ファイルをコピーしました");
        } catch (IOException e) {
            System.out.println("コピーエラー: " + e.getMessage());
        }
        // writerが先にクローズされ、次にreaderがクローズされる
    }
}

Java 9での拡張

Java 9以降では、try-with-resourcesでfinalまたは実質的finalな変数を使用できるようになりました。

1
2
3
4
5
6
7
// Java 9以降
public void processFile(BufferedReader reader) throws IOException {
    try (reader) { // 既存の変数を直接使用可能
        String line = reader.readLine();
        System.out.println(line);
    }
}

throwによる例外のスロー

throw文を使用すると、意図的に例外を発生させることができます。

基本構文

1
throw new 例外クラス("エラーメッセージ");

引数の検証での使用

メソッドの引数を検証し、不正な値が渡された場合に例外をスローするのは一般的なパターンです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThrowDemo {
    
    public static void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("年齢は0以上である必要があります: " + age);
        }
        if (age > 150) {
            throw new IllegalArgumentException("年齢が不正です: " + age);
        }
        System.out.println("年齢を " + age + " に設定しました");
    }
    
    public static void main(String[] args) {
        try {
            setAge(25);  // 正常
            setAge(-5);  // 例外発生
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

nullチェックでの使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class NullCheckDemo {
    
    public static void processData(String data) {
        if (data == null) {
            throw new NullPointerException("dataはnullにできません");
        }
        if (data.isEmpty()) {
            throw new IllegalArgumentException("dataは空文字にできません");
        }
        System.out.println("データを処理: " + data);
    }
}

Objects.requireNonNull()の活用

Java 7以降では、Objects.requireNonNull()を使用してnullチェックを簡潔に記述できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import java.util.Objects;

public class RequireNonNullDemo {
    private String name;
    
    public RequireNonNullDemo(String name) {
        // nullの場合、NullPointerExceptionをスロー
        this.name = Objects.requireNonNull(name, "nameはnullにできません");
    }
}

throwsによる例外の宣言

throwsキーワードを使用すると、メソッドが特定の例外をスローする可能性があることを宣言できます。

基本構文

1
2
3
public void メソッド名() throws 例外クラス1, 例外クラス2 {
    // 処理
}

throwsの使用例

検査例外を処理せずに呼び出し元に伝播させる場合、throwsを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;

public class ThrowsDemo {
    
    // 例外を呼び出し元に伝播させる
    public static String readFirstLine(String path) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(path));
        return reader.readLine();
    }
    
    // 呼び出し元で処理するか、さらに伝播させる
    public static void main(String[] args) {
        try {
            String line = readFirstLine("test.txt");
            System.out.println("最初の行: " + line);
        } catch (IOException e) {
            System.out.println("ファイル読み込みエラー: " + e.getMessage());
        }
    }
}

throwとthrowsの違い

キーワード 役割 使用場所
throw 例外を実際に発生させる メソッドの本体内
throws 例外をスローする可能性を宣言 メソッドのシグネチャ
1
2
3
4
5
6
// throwsで例外の可能性を宣言し、throw文で実際に例外を発生させる
public void validate(int value) throws IllegalArgumentException {
    if (value < 0) {
        throw new IllegalArgumentException("負の値は許可されていません");
    }
}

例外の伝播

例外はメソッドの呼び出しスタックを遡って伝播し、どこかで処理されるか、最終的にプログラムがクラッシュします。

sequenceDiagram
    participant main
    participant methodA
    participant methodB
    participant methodC
    
    main->>methodA: 呼び出し
    methodA->>methodB: 呼び出し
    methodB->>methodC: 呼び出し
    methodC-->>methodB: 例外発生
    Note over methodB: catchなし(throws宣言)
    methodB-->>methodA: 例外伝播
    Note over methodA: catchなし(throws宣言)
    methodA-->>main: 例外伝播
    Note over main: catchで処理

カスタム例外クラスの作成

Javaの標準例外クラスでは表現しきれない特定のエラー状況を表すために、独自の例外クラスを作成できます。

検査例外の作成

Exceptionクラスを継承して検査例外を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 検査例外として作成
public class InsufficientFundsException extends Exception {
    private final double amount;
    private final double balance;
    
    public InsufficientFundsException(double amount, double balance) {
        super(String.format("残高不足: 引き出し額 %.2f円、残高 %.2f円", amount, balance));
        this.amount = amount;
        this.balance = balance;
    }
    
    public double getAmount() {
        return amount;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public double getShortfall() {
        return amount - balance;
    }
}

非検査例外の作成

RuntimeExceptionクラスを継承して非検査例外を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 非検査例外として作成
public class InvalidUserIdException extends RuntimeException {
    private final String userId;
    
    public InvalidUserIdException(String userId) {
        super("不正なユーザーID: " + userId);
        this.userId = userId;
    }
    
    public InvalidUserIdException(String userId, Throwable cause) {
        super("不正なユーザーID: " + userId, cause);
        this.userId = userId;
    }
    
    public String getUserId() {
        return userId;
    }
}

カスタム例外の使用例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class BankAccount {
    private String accountNumber;
    private double balance;
    
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    
    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount <= 0) {
            throw new IllegalArgumentException("引き出し額は正の数である必要があります");
        }
        if (amount > balance) {
            throw new InsufficientFundsException(amount, balance);
        }
        balance -= amount;
        System.out.printf("%.2f円を引き出しました。残高: %.2f円%n", amount, balance);
    }
    
    public static void main(String[] args) {
        BankAccount account = new BankAccount("12345", 10000);
        
        try {
            account.withdraw(5000);   // 成功
            account.withdraw(8000);   // 残高不足
        } catch (InsufficientFundsException e) {
            System.out.println("エラー: " + e.getMessage());
            System.out.printf("不足額: %.2f円%n", e.getShortfall());
        }
    }
}

カスタム例外のベストプラクティス

プラクティス 説明
意味のある名前 例外の内容が分かる名前をつける(〜Exception)
適切な親クラス 回復可能なら検査例外、バグなら非検査例外
有用な情報を保持 エラー関連のデータをフィールドとして保持
コンストラクタの充実 cause(原因例外)を受け取るコンストラクタを用意

例外の再スロー

キャッチした例外をログに記録した後、再度スローして上位に伝播させることができます。

同じ例外を再スロー

1
2
3
4
5
6
7
8
public void processFile(String path) throws IOException {
    try {
        // ファイル処理
    } catch (IOException e) {
        System.err.println("ファイル処理エラー: " + e.getMessage());
        throw e; // 同じ例外を再スロー
    }
}

例外チェーンの活用

低レベルの例外を上位レベルの例外でラップして再スローすることで、詳細な情報を保持しながら抽象化できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class UserService {
    
    public User findUser(String id) throws UserNotFoundException {
        try {
            // データベースアクセス
            return database.findById(id);
        } catch (SQLException e) {
            // SQLExceptionをUserNotFoundExceptionでラップ
            throw new UserNotFoundException("ユーザー検索に失敗: " + id, e);
        }
    }
}

// カスタム例外
public class UserNotFoundException extends Exception {
    public UserNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

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

堅牢なプログラムを作成するための例外処理の指針をまとめます。

やるべきこと

プラクティス 説明
具体的な例外をキャッチ Exceptionより具体的な例外クラスをキャッチする
早期にfail 不正な状態は早期に検出して例外をスロー
適切なログ出力 例外情報をログに記録する
リソースは確実に解放 try-with-resourcesを活用する
意味のあるメッセージ デバッグに役立つ情報を含める

避けるべきこと

アンチパターン 問題点
空のcatchブロック 例外が無視され、バグの発見が困難に
Exceptionの大雑把なキャッチ 想定外の例外も一緒にキャッチしてしまう
制御フローでの例外使用 パフォーマンス低下と可読性の低下
スタックトレースの握りつぶし 原因調査が困難に

悪い例と良い例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 悪い例1: 空のcatchブロック
try {
    riskyOperation();
} catch (Exception e) {
    // 何もしない - バグの温床!
}

// 悪い例2: 例外を握りつぶしてnullを返す
public User findUser(String id) {
    try {
        return database.findById(id);
    } catch (SQLException e) {
        return null; // 呼び出し元でNullPointerExceptionの原因に
    }
}

// 良い例1: 適切なログ出力と再スロー
try {
    riskyOperation();
} catch (SpecificException e) {
    logger.error("操作に失敗しました", e);
    throw new ServiceException("サービス処理に失敗しました", e);
}

// 良い例2: Optionalを使用した安全な戻り値
public Optional<User> findUser(String id) {
    try {
        return Optional.of(database.findById(id));
    } catch (SQLException e) {
        logger.warn("ユーザー検索に失敗: {}", id, e);
        return Optional.empty();
    }
}

例外処理の設計指針

flowchart TD
    A[例外発生] --> B{回復可能か?}
    B -->|はい| C{ここで処理すべきか?}
    B -->|いいえ| D[ログを記録]
    D --> E[上位層に伝播]
    C -->|はい| F[適切に処理]
    C -->|いいえ| G[ラップして再スロー]
    F --> H[処理継続]
    G --> E

まとめ

この記事では、Javaの例外処理について体系的に解説しました。

学習のポイント

  1. 例外の分類: 検査例外は外部要因のエラー、非検査例外はプログラミングミス
  2. try-catch-finally: 例外を処理するための基本構文
  3. try-with-resources: リソースの自動解放を実現するモダンな構文
  4. throw/throws: 例外のスローと宣言の違い
  5. カスタム例外: ドメイン固有のエラーを表現する独自例外クラス
  6. ベストプラクティス: 堅牢なコードのための例外処理の指針

例外処理を適切に行うことで、予期しないエラーが発生してもプログラムが安全に動作し続ける堅牢なアプリケーションを構築できます。最初は複雑に感じるかもしれませんが、実践を通じて徐々に身につけていきましょう。

参考リンク