はじめに#
プログラムの実行中には、ファイルが見つからない、ネットワーク接続が切断された、不正な入力が渡されたなど、さまざまな想定外の事態が発生する可能性があります。このような状況を適切に処理しないと、プログラムが突然クラッシュし、ユーザーに不便を与えることになります。
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 <|-- IllegalArgumentExceptionThrowableとErrorとExceptionの違い#
| クラス |
説明 |
対処方法 |
Throwable |
すべての例外とエラーの基底クラス |
直接使用することは少ない |
Error |
JVMレベルの深刻な問題 |
通常はキャッチしない |
Exception |
プログラムで回復可能な問題 |
適切にキャッチして処理 |
ErrorはOutOfMemoryErrorやStackOverflowErrorなど、アプリケーションで回復が困難な問題を表します。一方、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の例外処理について体系的に解説しました。
学習のポイント#
- 例外の分類: 検査例外は外部要因のエラー、非検査例外はプログラミングミス
- try-catch-finally: 例外を処理するための基本構文
- try-with-resources: リソースの自動解放を実現するモダンな構文
- throw/throws: 例外のスローと宣言の違い
- カスタム例外: ドメイン固有のエラーを表現する独自例外クラス
- ベストプラクティス: 堅牢なコードのための例外処理の指針
例外処理を適切に行うことで、予期しないエラーが発生してもプログラムが安全に動作し続ける堅牢なアプリケーションを構築できます。最初は複雑に感じるかもしれませんが、実践を通じて徐々に身につけていきましょう。
参考リンク#