例外処理
この章では、プログラム実行中に発生するエラーを適切に処理する 例外処理 について学ぶ。 例外処理を使うことで、エラーが発生してもプログラムが適切に動作し続けるようにできる。
この章で得られるスキル:
- ✅ エラーが起きてもプログラムを止めずに対処できる
- ✅ ユーザーに分かりやすいエラーメッセージを表示できる
- ✅ ファイルやネットワークなど失敗する可能性がある処理を安全に実装できる
- ✅ 実務レベルの堅牢なプログラムが書ける
Step 0: 例外処理がないとどうなる?
問題:ユーザーが間違った入力をした場合
Webアプリケーションで、ユーザーが年齢を入力するフォームがあるとする。 ユーザーが数字以外を入力した場合、どうなるか?
例外処理がない場合
public class UserRegistration {
public static void main(String[] args) {
String userInput = "二十歳"; // ユーザーが数字以外を入力
// 文字列を整数に変換
int age = Integer.parseInt(userInput); // ここでプログラムが停止!
// この先のコードは実行されない
System.out.println("年齢: " + age + "歳");
System.out.println("登録が完了しました");
}
}
実行結果:
Exception in thread "main" java.lang.NumberFormatException: For input string: "二十歳"
プログラムが強制終了し、ユーザーには意味不明なエラーメッセージが表示される。
例外処理がある場合
public class UserRegistration {
public static void main(String[] args) {
String userInput = "二十歳";
try {
int age = Integer.parseInt(userInput);
System.out.println("年齢: " + age + "歳");
System.out.println("登録が完了しました");
} catch (NumberFormatException e) {
System.out.println("エラー: 年齢は数字で入力してください");
System.out.println("入力された値: " + userInput);
}
// プログラムは継続できる
System.out.println("システムは正常に動作しています");
}
}
実行結果:
エラー: 年齢は数字で入力してください
入力された値: 二十歳
システムは正常に動作しています
ユーザーにわかりやすいメッセージを表示し、プログラムは継続できる。
例外処理がないとどうなるか:
- プログラムが途中で停止する
- ユーザーに意味不明なエラーメッセージが表示される
- データが中途半端な状態で残る
- システム全体が停止する可能性がある
例外処理があると:
- エラーが発生してもプログラムが停止しない
- ユーザーにわかりやすいメッセージを表示できる
- エラーから回復して処理を続行できる
- ログを記録してデバッグできる
では、例外処理の使い方を詳しく学んでいこう。
Step 1: 例外とは
例外の概念
例外(Exception) は、 プログラム実行中に発生する予期しない問題 である。
プログラムの「通常の流れ」から外れた「例外的な状況」を表す。
よくある例外
1. ArithmeticException(算術例外)
0で割ろうとした時に発生する。
int result = 10 / 0; // ArithmeticException
2. NullPointerException(ヌルポインタ例外)
nullのオブジェクトのメソッドやフィールドにアクセスしようとした時に発生する。
String text = null;
int length = text.length(); // NullPointerException
3. ArrayIndexOutOfBoundsException(配列の範囲外例外)
配列の存在しないインデックスにアクセスしようとした時に発生する。
int[] numbers = {1, 2, 3};
int value = numbers[10]; // ArrayIndexOutOfBoundsException
4. NumberFormatException(数値変換例外)
文字列を数値に変換できない時に発生する。
int number = Integer.parseInt("abc"); // NumberFormatException
5. ClassCastException(型変換例外)
不正な型変換をしようとした時に発生する。
Object obj = "文字列";
Integer num = (Integer) obj; // ClassCastException
実行してみよう:
やってみよう:
- 各例外の
getMessage()の内容を確認してみよう e.printStackTrace()を追加して、スタックトレースを表示してみよう
Step 2: try-catch文の基本
基本的な書き方
try {
// 例外が発生する可能性のあるコード
} catch (例外の型 変数名) {
// 例外が発生した時の処理
}
処理の流れ
tryブロック のコードを実行する- 例外が発生しなければ、
catchブロックはスキップされる - 例外が発生すると、直ちに
catchブロックに移動する tryブロックの残りのコードは実行されないcatchブロックの処理が終わったら、try-catchの後のコードが実行される
例:0で割る例外を捕捉
System.out.println("プログラム開始");
try {
int a = 10;
int b = 0;
int result = a / b; // ここで例外が発生
System.out.println("結果: " + result); // この行は実行されない
} catch (ArithmeticException e) {
System.out.println("エラー: 0で割ることはできません");
}
System.out.println("プログラム終了"); // ここは実行される
例外オブジェクトのメソッド
catchブロックで受け取った例外オブジェクトeには、便利なメソッドがある:
| メソッド | 説明 |
|---|---|
getMessage() | エラーメッセージを取得 |
toString() | 例外の型とメッセージを取得 |
printStackTrace() | スタックトレース(エラーの発生場所)を表示 |
実行してみよう:
やってみよう:
bを2に変更して、例外が発生しない場合の動作を確認しようSystem.out.println()を追加して、処理の流れを詳しく観察しよう
Step 3: 複数の例外を捕捉
複数のcatchブロック
異なる種類の例外を、それぞれ異なる方法で処理できる。
try {
// 例外が発生する可能性のあるコード
} catch (例外の型1 変数名1) {
// 例外1が発生した時の処理
} catch (例外の型2 変数名2) {
// 例外2が発生した時の処理
} catch (例外の型3 変数名3) {
// 例外3が発生した時の処理
}
例:複数の例外を処理
try {
String[] data = {"10", "20", "abc"};
int index = 1;
String value = data[index];
int number = Integer.parseInt(value);
int result = 100 / number;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("エラー: 配列の範囲外です");
} catch (NumberFormatException e) {
System.out.println("エラー: 数値に変換できません");
} catch (ArithmeticException e) {
System.out.println("エラー: 0で割ることはできません");
}
例外の順序
複数のcatchブロックを書く場合、 より具体的な例外を先に書く 必要がある。
親クラスの例外(例:Exception)を先に書くと、子クラスの例外(例:IOException)が捕捉されなくなる。
悪い例(コンパイルエラー):
try {
// ...
} catch (Exception e) { // 先に親クラス
// ...
} catch (IOException e) { // エラー!この行には到達しない
// ...
}
良い例:
try {
// ...
} catch (IOException e) { // 先に子クラス
// ...
} catch (Exception e) { // 後に親クラス
// ...
}
実行してみよう:
やってみよう:
testCasesに新しいテストケースを追加してみよう- 配列の範囲外アクセスを試して、
ArrayIndexOutOfBoundsExceptionを捕捉してみよう
Step 4: finally句
finallyとは
finally句 は、 例外の有無に関わらず必ず実行される ブロックである。
try {
// 例外が発生する可能性のあるコード
} catch (例外の型 変数名) {
// 例外が発生した時の処理
} finally {
// 必ず実行される処理
}
finallyの実行タイミング
| 状況 | tryブロック | catchブロック | finallyブロック |
|---|---|---|---|
| 例外が発生しない | 実行される | 実行されない | 実行される |
| 例外が発生する | 途中まで実行 | 実行される | 実行される |
finallyの用途
主な用途:リソースの解放
- ファイルのクローズ
- データベース接続の解放
- ネットワーク接続のクローズ
- ロックの解放
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// ファイルを読む処理
} catch (IOException e) {
System.out.println("ファイル読み込みエラー");
} finally {
if (reader != null) {
reader.close(); // 必ずファイルを閉じる
}
}
finallyブロックは、 return文があっても実行される。
つまり、メソッドから抜ける直前でも、finallyブロックが実行される。
実行してみよう:
やってみよう:
finallyブロックを削除して、動作の違いを確認しようreturn文を追加して、finallyブロックが実行されることを確認しよう
Step 5: throwsキーワード
throwsとは
throwsキーワード は、 メソッドが例外をスローする可能性があることを宣言する キーワードである。
戻り値の型 メソッド名(引数) throws 例外の型1, 例外の型2 {
// 例外が発生する可能性のあるコード
}
throwsの役割
例外処理の責任を 呼び出し側に委譲 する。
メソッド側:
int divide(int a, int b) throws ArithmeticException {
return a / b; // 例外が発生する可能性がある
}
呼び出し側:
try {
int result = divide(10, 0); // 呼び出し側で例外を処理する
} catch (ArithmeticException e) {
System.out.println("エラー: " + e.getMessage());
}
なぜthrowsを使うのか?
-
メソッドの責任を明確にする
- このメソッドは例外が発生する可能性があることを示す
-
例外処理を呼び出し側に任せる
- メソッド内で例外を処理するか、呼び出し側で処理するかを選べる
-
コンパイラによるチェック
- 検査例外(後述)の場合、呼び出し側で処理を強制される
実行してみよう:
やってみよう:
Calculatorクラスに新しいメソッドを追加してみよう- 複数の例外をスローするメソッドを作ってみよう(
throws Exception1, Exception2)
Step 6: 例外をスローする(throw)
throwキーワード
throwキーワード で、意図的に例外をスローできる。
if (不正な条件) {
throw new 例外の型("エラーメッセージ");
}
throwとthrowsの違い
throw | throws |
|---|---|
| 例外を 発生させる | 例外を 宣言する |
| メソッド内で使用 | メソッドのシグネチャで使用 |
throw new Exception() | void method() throws Exception |
例:入力値を検証して例外をスロー
void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上である必要があります");
}
if (age > 150) {
throw new IllegalArgumentException("年齢は150以下である必要があります");
}
this.age = age;
}
よく使う例外クラス
| 例外クラス | 用途 |
|---|---|
IllegalArgumentException | 引数が不正な場合 |
IllegalStateException | オブジェクトの状態が不正な場合 |
NullPointerException | nullが渡された場合 |
UnsupportedOperationException | サポートされていない操作の場合 |
実行してみよう:
やってみよう:
- 新しい検証ルールを追加してみよう(例:名前の長さ制限)
- パスワードの検証メソッドを追加してみよう
Step 7: 検査例外と非検査例外
2種類の例外
Javaの例外には、 検査例外(Checked Exception) と 非検査例外(Unchecked Exception) の2種類がある。
| 種類 | 説明 | 例 | 処理の義務 |
|---|---|---|---|
| 検査例外 | コンパイル時にチェックされる 外部要因で発生する | IOExceptionSQLExceptionFileNotFoundException | 必須 (try-catchまたはthrows) |
| 非検査例外 | 実行時に発生する プログラムのバグで発生する | NullPointerExceptionArithmeticExceptionArrayIndexOutOfBoundsException | 任意 |
例外クラスの階層
Throwable
├─ Error(システムエラー、通常は処理しない)
│ ├─ OutOfMemoryError
│ └─ StackOverflowError
└─ Exception
├─ RuntimeException(非検査例外)
│ ├─ NullPointerException
│ ├─ ArithmeticException
│ ├─ ArrayIndexOutOfBoundsException
│ ├─ IllegalArgumentException
│ └─ NumberFormatException
└─ IOException(検査例外)
├─ FileNotFoundException
└─ EOFException
検査例外 vs 非検査例外
検査例外(Checked Exception)
特徴:
- コンパイラが処理を強制する
- 外部要因(ファイル、ネットワーク、データベース)で発生する
- 予測可能で、回復可能なエラー
例:
// ファイルを読む(IOException は検査例外)
void readFile(String filename) throws IOException {
FileReader reader = new FileReader(filename); // 検査例外が発生する可能性
// ...
}
// 呼び出し側で処理が必須
try {
readFile("data.txt");
} catch (IOException e) {
// 処理しないとコンパイルエラー
}
非検査例外(Unchecked Exception)
特徴:
- コンパイラがチェックしない
- プログラムのバグ(論理エラー)で発生する
- 予測不可能、回復不可能なエラー
例:
// 配列の範囲外アクセス(ArrayIndexOutOfBoundsException は非検査例外)
int[] numbers = {1, 2, 3};
int value = numbers[10]; // try-catchは不要(バグを修正すべき)
なぜ2種類に分けるのか?
| 検査例外 | 非検査例外 |
|---|---|
| 外部要因 で発生 (ファイルが存在しない、ネットワークエラー) | プログラムのバグ で発生 (null参照、配列の範囲外) |
| 回復可能 (別のファイルを読む、リトライする) | 回復不可能 (バグを修正する必要がある) |
| 処理を強制 (エラー処理の漏れを防ぐ) | 処理は任意 (バグを修正すれば発生しない) |
検査例外は実務で頻繁に遭遇する。
ファイルの読み書き、データベースアクセス、ネットワーク通信など、外部リソースを扱う処理では必ず検査例外が発生する可能性がある。
コンパイラが処理を強制するため、初学者には面倒に感じるかもしれないが、これによりエラー処理の漏れを防ぐことができる。
実務では、適切な例外処理がシステムの信頼性を大きく左右する。
実行してみよう:
やってみよう:
- 検査例外と非検査例外の違いを整理してみよう
- 実務でどのような場面で検査例外を使うか考えてみよう
Step 8: 実践課題
課題1:ユーザー入力検証システム
ユーザーの入力を検証し、不正な入力に対して適切なエラーメッセージを表示するシステムを作成せよ。
要件:
- 名前、年齢、メールアドレスを入力として受け取る
- 各入力に対して以下の検証を行う:
- 名前:空でない、1文字以上20文字以下
- 年齢:0以上120以下の整数
- メールアドレス:
@を含む
- 不正な入力の場合、
IllegalArgumentExceptionをスローする - すべての検証が通った場合、「登録成功」と表示する
サンプルデータ:
String[][] testData = {
{"太郎", "25", "taro@example.com"}, // 正常
{"", "30", "test@example.com"}, // 名前が空
{"花子", "abc", "hanako@example.com"}, // 年齢が数字でない
{"次郎", "-5", "jiro@example.com"}, // 年齢が負
{"美咲", "22", "invalid-email"} // メールアドレスが不正
};
課題2:銀行口座システム
銀行口座の入金・出金を管理し、不正な操作に対して例外をスローするシステムを作成せよ。
要件:
- 口座番号、口座名義、残高を管理する
- 以下のメソッドを実装する:
deposit(int amount):入金withdraw(int amount):出金getBalance():残高照会
- 以下の場合に例外をスローする:
- 入金額が0以下:
IllegalArgumentException - 出金額が0以下:
IllegalArgumentException - 残高不足:
IllegalStateException
- 入金額が0以下:
- 取引履歴を表示する
課題3:配列の安全なアクセス
配列に安全にアクセスするユーティリティクラスを作成せよ。
要件:
safeGet(int[] array, int index):配列の要素を安全に取得- インデックスが範囲外の場合、デフォルト値(0)を返す
safeSet(int[] array, int index, int value):配列の要素を安全に設定- インデックスが範囲外の場合、何もしない
sum(int[] array):配列の合計を計算nullの場合、0を返す
- すべてのメソッドで例外を適切に処理する
まとめ
この章では、Javaの 例外処理 について学んだ。
学んだ内容
- 例外 はプログラム実行中に発生する予期しない問題である
try-catch文 で例外を捕捉し、適切に処理できるtryブロック:例外が発生する可能性のあるコードcatchブロック:例外が発生した時の処理
- 複数の
catchブロックで異なる例外を処理できる- より具体的な例外を先に書く
finally句 は例外の有無に関わらず必ず実行される- リソースの解放に使う
throwsキーワード でメソッドが例外をスローすることを宣言できる- 例外処理の責任を呼び出し側に委譲する
throwキーワード で意図的に例外をスローできる- 入力値の検証などに使う
- 検査例外 はコンパイル時にチェックされ、処理が必須である
- 外部要因で発生する(ファイル、ネットワーク、データベース)
- 非検査例外 は実行時に発生し、処理は任意である
- プログラムのバグで発生する(null参照、配列の範囲外)
- 例外処理により、エラーが発生してもプログラムを適切に制御できる
次のステップ
次の章では、 オブジェクト指向の基礎 について学ぶ。 クラスとオブジェクトの概念を理解し、より実用的で保守性の高いプログラムを作る方法を学ぶ。
FAQ
Q1: ExceptionとErrorの違いは?
A: Javaには Throwable の下に Exception と Error の2系統がある。
| Exception | Error |
|---|---|
| プログラムで対処可能なエラー | プログラムで対処不可能なエラー |
| ファイルが見つからない、0で割る | メモリ不足、スタックオーバーフロー |
| try-catchで処理すべき | 通常は処理しない |
Errorの例:
OutOfMemoryError:メモリ不足StackOverflowError:スタックオーバーフローVirtualMachineError:JVM内部エラー
Errorは システムレベルの深刻な問題 であり、通常はプログラムで回復できない。
Exceptionは アプリケーションレベルの問題 であり、適切に処理すれば回復できる。
Q2: 例外を握りつぶしてはいけないのはなぜ?
A: 例外を握りつぶす(catch して何もしない)と、以下の問題が発生する。
悪い例:
try {
// 重要な処理
} catch (Exception e) {
// 何もしない → 例外を握りつぶしている
}
問題点:
-
エラーが隠蔽される
- エラーが発生したことに気づけない
- バグの発見が遅れる
-
デバッグが困難になる
- エラーの原因がわからない
- ログが残らない
-
データの不整合が発生する
- 処理が中途半端な状態で終わる
- 後続の処理に影響する
良い例:
try {
// 重要な処理
} catch (Exception e) {
System.err.println("エラー: " + e.getMessage());
e.printStackTrace(); // スタックトレースを出力
// または、ログに記録する
}
最低限やるべきこと:
- エラーメッセージを出力する
- ログに記録する
- 必要に応じて再スロー(
throw e;)する
Q3: try-with-resources文とは?
A: try-with-resources文 は、リソースを自動的にクローズする構文である(Java 7以降)。
従来の方法(finallyを使う):
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// ファイルを読む処理
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close(); // 手動でクローズ
} catch (IOException e) {
e.printStackTrace();
}
}
}
try-with-resources(推奨):
try (FileReader reader = new FileReader("file.txt")) {
// ファイルを読む処理
} catch (IOException e) {
e.printStackTrace();
}
// reader は自動的にクローズされる
メリット:
- コードが簡潔になる
- クローズ忘れを防げる
- 複数のリソースを同時に管理できる
条件:
- リソースが
AutoCloseableインターフェースを実装している必要がある
Q4: カスタム例外を作る意味は?
A: カスタム例外(独自の例外クラス)を作ることで、以下のメリットがある。
1. エラーの種類を明確にする
標準の例外だけでは、エラーの詳細がわからない場合がある。
// 標準の例外
throw new Exception("残高不足です"); // 曖昧
// カスタム例外
throw new InsufficientBalanceException("残高不足です"); // 明確
2. エラー処理を細かく制御できる
try {
// 銀行の処理
} catch (InsufficientBalanceException e) {
// 残高不足の場合の処理
} catch (InvalidAccountException e) {
// 口座が無効な場合の処理
}
3. ビジネスロジックを表現できる
class InsufficientBalanceException extends Exception {
private int balance;
private int requestedAmount;
InsufficientBalanceException(int balance, int requestedAmount) {
super("残高不足: 残高=" + balance + "円, 要求額=" + requestedAmount + "円");
this.balance = balance;
this.requestedAmount = requestedAmount;
}
int getShortfall() {
return requestedAmount - balance;
}
}
カスタム例外の作り方:
// 検査例外を作る場合
class MyCheckedException extends Exception {
MyCheckedException(String message) {
super(message);
}
}
// 非検査例外を作る場合
class MyUncheckedException extends RuntimeException {
MyUncheckedException(String message) {
super(message);
}
}
Q5: 例外処理はパフォーマンスに影響するか?
A: はい、影響する。 ただし、適切に使えば問題ない。
例外処理のコスト:
-
例外をスローするコスト
- スタックトレースの生成に時間がかかる
- 通常の処理の 100〜1000倍 遅い
-
try-catchのコスト
- 例外が発生しなければ、ほぼコストなし
- 例外が発生した場合のみコストがかかる
ベストプラクティス:
❌ 悪い例:例外を制御フローに使う
// 例外を使ってループを抜ける(悪い例)
try {
for (int i = 0; ; i++) {
System.out.println(array[i]);
}
} catch (ArrayIndexOutOfBoundsException e) {
// 配列の終わりに到達
}
✅ 良い例:通常の制御構文を使う
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
例外処理を使うべき場合:
- 予期しないエラー が発生する可能性がある場合
- 外部リソース を扱う場合(ファイル、ネットワーク、データベース)
- 回復可能なエラー を処理する場合
例外処理を使うべきでない場合:
- 通常の制御フロー を実装する場合
- 予測可能なエラー を処理する場合(事前チェックで回避できる)
- パフォーマンスが重要 な処理で頻繁に例外が発生する場合
結論: 例外処理は「例外的な状況」にのみ使い、通常の制御フローには使わない。