Skip to main content

例外処理

この章では、プログラム実行中に発生するエラーを適切に処理する 例外処理 について学ぶ。 例外処理を使うことで、エラーが発生してもプログラムが適切に動作し続けるようにできる。


この章で得られるスキル:

  • ✅ エラーが起きてもプログラムを止めずに対処できる
  • ✅ ユーザーに分かりやすいエラーメッセージを表示できる
  • ✅ ファイルやネットワークなど失敗する可能性がある処理を安全に実装できる
  • ✅ 実務レベルの堅牢なプログラムが書ける

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

実行してみよう:

public class CommonExceptions { public static void main(String[] args) { System.out.println("=== よくある例外の例 ===\n"); // 1. ArithmeticException System.out.println("1. ArithmeticException:"); try { int result = 10 / 0; } catch (ArithmeticException e) { System.out.println("エラー: " + e.getClass().getSimpleName()); System.out.println("メッセージ: " + e.getMessage()); } System.out.println("\n---\n"); // 2. NullPointerException System.out.println("2. NullPointerException:"); try { String text = null; int length = text.length(); } catch (NullPointerException e) { System.out.println("エラー: " + e.getClass().getSimpleName()); } System.out.println("\n---\n"); // 3. ArrayIndexOutOfBoundsException System.out.println("3. ArrayIndexOutOfBoundsException:"); try { int[] numbers = {1, 2, 3}; int value = numbers[10]; } catch (ArrayIndexOutOfBoundsException e) { System.out.println("エラー: " + e.getClass().getSimpleName()); System.out.println("メッセージ: " + e.getMessage()); } System.out.println("\n---\n"); // 4. NumberFormatException System.out.println("4. NumberFormatException:"); try { int number = Integer.parseInt("abc"); } catch (NumberFormatException e) { System.out.println("エラー: " + e.getClass().getSimpleName()); System.out.println("メッセージ: " + e.getMessage()); } } }

やってみよう:

  • 各例外のgetMessage()の内容を確認してみよう
  • e.printStackTrace()を追加して、スタックトレースを表示してみよう

Step 2: try-catch文の基本

基本的な書き方

try {
// 例外が発生する可能性のあるコード
} catch (例外の型 変数名) {
// 例外が発生した時の処理
}

処理の流れ

  1. tryブロック のコードを実行する
  2. 例外が発生しなければ、catchブロックはスキップされる
  3. 例外が発生すると、直ちにcatchブロックに移動する
  4. tryブロックの残りのコードは実行されない
  5. 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()スタックトレース(エラーの発生場所)を表示

実行してみよう:

public class TryCatchBasics { public static void main(String[] args) { System.out.println("プログラム開始"); try { System.out.println("tryブロック: 計算開始"); int a = 10; int b = 0; int result = a / b; // ここで例外が発生 System.out.println("tryブロック: 結果 = " + result); // 実行されない System.out.println("tryブロック: 計算終了"); // 実行されない } catch (ArithmeticException e) { System.out.println("\ncatchブロック: 例外を捕捉しました"); System.out.println("例外の型: " + e.getClass().getSimpleName()); System.out.println("メッセージ: " + e.getMessage()); } System.out.println("\nプログラム終了(正常に継続)"); } }

やってみよう:

  • b2に変更して、例外が発生しない場合の動作を確認しよう
  • 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) { // 後に親クラス
// ...
}

実行してみよう:

public class MultipleCatch { public static void main(String[] args) { String[] testCases = { "10", // 正常 "abc", // NumberFormatException "0" // ArithmeticException }; for (int i = 0; i < testCases.length; i++) { System.out.println("\n=== テストケース " + (i + 1) + ": " + testCases[i] + " ==="); try { String input = testCases[i]; int number = Integer.parseInt(input); int result = 100 / number; System.out.println("成功: 100 / " + number + " = " + result); } catch (NumberFormatException e) { System.out.println("エラー: 数値に変換できません"); System.out.println("入力: " + testCases[i]); } catch (ArithmeticException e) { System.out.println("エラー: 0で割ることはできません"); } } System.out.println("\n処理完了"); } }

やってみよう:

  • 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は強力

finallyブロックは、 return文があっても実行される

つまり、メソッドから抜ける直前でも、finallyブロックが実行される。

実行してみよう:

public class FinallyExample { public static void main(String[] args) { System.out.println("=== ケース1: 例外が発生する場合 ==="); testCase1(); System.out.println("\n=== ケース2: 例外が発生しない場合 ==="); testCase2(); } static void testCase1() { try { System.out.println("tryブロック: 開始"); int result = 10 / 0; // 例外が発生 System.out.println("tryブロック: 終了"); // 実行されない } catch (ArithmeticException e) { System.out.println("catchブロック: 例外を捕捉"); } finally { System.out.println("finallyブロック: 必ず実行"); } System.out.println("メソッド終了"); } static void testCase2() { try { System.out.println("tryブロック: 開始"); int result = 10 / 2; System.out.println("tryブロック: 終了"); } catch (ArithmeticException e) { System.out.println("catchブロック: 例外を捕捉"); // 実行されない } finally { System.out.println("finallyブロック: 必ず実行"); } System.out.println("メソッド終了"); } }

やってみよう:

  • 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を使うのか?

  1. メソッドの責任を明確にする

    • このメソッドは例外が発生する可能性があることを示す
  2. 例外処理を呼び出し側に任せる

    • メソッド内で例外を処理するか、呼び出し側で処理するかを選べる
  3. コンパイラによるチェック

    • 検査例外(後述)の場合、呼び出し側で処理を強制される

実行してみよう:

class Calculator { // throwsで例外をスローする可能性を宣言 int divide(int a, int b) throws ArithmeticException { if (b == 0) { throw new ArithmeticException("0で割ることはできません"); } return a / b; } int multiply(int a, int b) { return a * b; // 例外は発生しない } } public class ThrowsExample { public static void main(String[] args) { Calculator calc = new Calculator(); // multiplyは例外をスローしないので、try-catchは不要 int result1 = calc.multiply(10, 2); System.out.println("10 * 2 = " + result1); System.out.println("---"); // divideは例外をスローする可能性があるので、try-catchが必要 try { int result2 = calc.divide(10, 2); System.out.println("10 / 2 = " + result2); int result3 = calc.divide(10, 0); // 例外が発生 System.out.println("10 / 0 = " + result3); // 実行されない } catch (ArithmeticException e) { System.out.println("エラー: " + e.getMessage()); } System.out.println("プログラム終了"); } }

やってみよう:

  • Calculatorクラスに新しいメソッドを追加してみよう
  • 複数の例外をスローするメソッドを作ってみよう(throws Exception1, Exception2

Step 6: 例外をスローする(throw)

throwキーワード

throwキーワード で、意図的に例外をスローできる。

if (不正な条件) {
throw new 例外の型("エラーメッセージ");
}

throwとthrowsの違い

throwthrows
例外を 発生させる例外を 宣言する
メソッド内で使用メソッドのシグネチャで使用
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オブジェクトの状態が不正な場合
NullPointerExceptionnullが渡された場合
UnsupportedOperationExceptionサポートされていない操作の場合

実行してみよう:

class User { private String name; private int age; private String email; void setName(String name) { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("名前は空にできません"); } this.name = name; } void setAge(int age) { if (age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります"); } if (age > 150) { throw new IllegalArgumentException("年齢は150以下である必要があります"); } this.age = age; } void setEmail(String email) { if (email == null || !email.contains("@")) { throw new IllegalArgumentException("正しいメールアドレスを入力してください"); } this.email = email; } @Override public String toString() { return "User[name=" + name + ", age=" + age + ", email=" + email + "]"; } } public class ThrowExample { public static void main(String[] args) { User user = new User(); // 正常なケース try { user.setName("太郎"); user.setAge(25); user.setEmail("taro@example.com"); System.out.println("成功: " + user); } catch (IllegalArgumentException e) { System.out.println("エラー: " + e.getMessage()); } System.out.println("---"); // 異常なケース String[][] testCases = { {"", "30", "test@example.com"}, // 名前が空 {"花子", "-5", "hanako@example.com"}, // 年齢が負 {"次郎", "200", "jiro@example.com"}, // 年齢が大きすぎ {"美咲", "22", "invalid-email"} // メールアドレスが不正 }; for (int i = 0; i < testCases.length; i++) { System.out.println("テストケース " + (i + 1) + ":"); User testUser = new User(); try { testUser.setName(testCases[i][0]); testUser.setAge(Integer.parseInt(testCases[i][1])); testUser.setEmail(testCases[i][2]); System.out.println(" 成功: " + testUser); } catch (IllegalArgumentException e) { System.out.println(" エラー: " + e.getMessage()); } } } }

やってみよう:

  • 新しい検証ルールを追加してみよう(例:名前の長さ制限)
  • パスワードの検証メソッドを追加してみよう

Step 7: 検査例外と非検査例外

2種類の例外

Javaの例外には、 検査例外(Checked Exception)非検査例外(Unchecked Exception) の2種類がある。

種類説明処理の義務
検査例外コンパイル時にチェックされる
外部要因で発生する
IOException
SQLException
FileNotFoundException
必須
(try-catchまたはthrows)
非検査例外実行時に発生する
プログラムのバグで発生する
NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
任意

例外クラスの階層

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参照、配列の範囲外)
回復可能
(別のファイルを読む、リトライする)
回復不可能
(バグを修正する必要がある)
処理を強制
(エラー処理の漏れを防ぐ)
処理は任意
(バグを修正すれば発生しない)
実務での重要性

検査例外は実務で頻繁に遭遇する。

ファイルの読み書き、データベースアクセス、ネットワーク通信など、外部リソースを扱う処理では必ず検査例外が発生する可能性がある。

コンパイラが処理を強制するため、初学者には面倒に感じるかもしれないが、これによりエラー処理の漏れを防ぐことができる。

実務では、適切な例外処理がシステムの信頼性を大きく左右する。

実行してみよう:

public class CheckedVsUnchecked { public static void main(String[] args) { System.out.println("=== 非検査例外の例 ==="); demoUnchecked(); System.out.println("\n=== 検査例外の例(疑似) ==="); demoChecked(); } // 非検査例外: try-catchは任意(バグを修正すべき) static void demoUnchecked() { try { // 例1: NullPointerException(非検査例外) String text = null; int length = text.length(); // バグ:nullチェックをしていない } catch (NullPointerException e) { System.out.println("エラー: NullPointerException"); System.out.println("→ これはバグです。nullチェックを追加すべきです。"); } } // 検査例外: try-catchまたはthrowsが必須(外部要因) static void demoChecked() { try { // 疑似的なファイル読み込み(実際のIOExceptionは使えないため) String filename = "data.txt"; readFilePseudo(filename); } catch (IllegalStateException e) { System.out.println("エラー: " + e.getMessage()); System.out.println("→ これは外部要因です。ファイルが存在しない可能性があります。"); } } // 疑似的なファイル読み込みメソッド static void readFilePseudo(String filename) { // 実際のファイル読み込みでは IOException(検査例外)が発生する if (!filename.equals("exists.txt")) { throw new IllegalStateException("ファイルが見つかりません: " + filename); } System.out.println("ファイルを読み込みました: " + filename); } }

やってみよう:

  • 検査例外と非検査例外の違いを整理してみよう
  • 実務でどのような場面で検査例外を使うか考えてみよう

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"} // メールアドレスが不正
};
class UserValidator { void validateName(String name) { // ここに検証コードを書く } void validateAge(String ageStr) { // ここに検証コードを書く } void validateEmail(String email) { // ここに検証コードを書く } void registerUser(String name, String ageStr, String email) { // ここに全体の検証と登録処理を書く } } public class UserInputValidation { public static void main(String[] args) { UserValidator validator = new UserValidator(); String[][] testData = { {"太郎", "25", "taro@example.com"}, {"", "30", "test@example.com"}, {"花子", "abc", "hanako@example.com"}, {"次郎", "-5", "jiro@example.com"}, {"美咲", "22", "invalid-email"} }; for (int i = 0; i < testData.length; i++) { System.out.println("\n=== テストケース " + (i + 1) + " ==="); System.out.println("入力: " + testData[i][0] + ", " + testData[i][1] + ", " + testData[i][2]); // ここにvalidator.registerUser()を呼び出すコードを書く } } }

課題2:銀行口座システム

銀行口座の入金・出金を管理し、不正な操作に対して例外をスローするシステムを作成せよ。

要件:

  • 口座番号、口座名義、残高を管理する
  • 以下のメソッドを実装する:
    • deposit(int amount):入金
    • withdraw(int amount):出金
    • getBalance():残高照会
  • 以下の場合に例外をスローする:
    • 入金額が0以下:IllegalArgumentException
    • 出金額が0以下:IllegalArgumentException
    • 残高不足:IllegalStateException
  • 取引履歴を表示する
class BankAccount { private String accountNumber; private String accountHolder; private int balance; BankAccount(String accountNumber, String accountHolder, int initialBalance) { this.accountNumber = accountNumber; this.accountHolder = accountHolder; this.balance = initialBalance; } void deposit(int amount) { // ここに入金処理を書く } void withdraw(int amount) { // ここに出金処理を書く } int getBalance() { return balance; } @Override public String toString() { return "口座[" + accountNumber + "] " + accountHolder + " 様 残高: " + balance + "円"; } } public class BankAccountSystem { public static void main(String[] args) { BankAccount account = new BankAccount("123-456", "山田太郎", 10000); System.out.println("初期状態: " + account); // ここに以下の操作を実装する: // 1. 5000円を入金 // 2. 3000円を出金 // 3. 20000円を出金(残高不足で例外が発生する) // 4. -1000円を入金(負の金額で例外が発生する) } }

課題3:配列の安全なアクセス

配列に安全にアクセスするユーティリティクラスを作成せよ。

要件:

  • safeGet(int[] array, int index):配列の要素を安全に取得
    • インデックスが範囲外の場合、デフォルト値(0)を返す
  • safeSet(int[] array, int index, int value):配列の要素を安全に設定
    • インデックスが範囲外の場合、何もしない
  • sum(int[] array):配列の合計を計算
    • nullの場合、0を返す
  • すべてのメソッドで例外を適切に処理する
class SafeArrayUtil { static int safeGet(int[] array, int index) { // ここに実装を書く return 0; // デフォルト値 } static void safeSet(int[] array, int index, int value) { // ここに実装を書く } static int sum(int[] array) { // ここに実装を書く return 0; } } public class SafeArrayAccess { public static void main(String[] args) { int[] numbers = {10, 20, 30, 40, 50}; // safeGetのテスト System.out.println("=== safeGetのテスト ==="); System.out.println("numbers[2] = " + SafeArrayUtil.safeGet(numbers, 2)); // 30 System.out.println("numbers[10] = " + SafeArrayUtil.safeGet(numbers, 10)); // 0(範囲外) System.out.println("numbers[-1] = " + SafeArrayUtil.safeGet(numbers, -1)); // 0(範囲外) System.out.println("\n=== safeSetのテスト ==="); SafeArrayUtil.safeSet(numbers, 2, 100); System.out.println("numbers[2] = " + numbers[2]); // 100 SafeArrayUtil.safeSet(numbers, 10, 999); // 何もしない System.out.println("配列: " + java.util.Arrays.toString(numbers)); System.out.println("\n=== sumのテスト ==="); System.out.println("合計: " + SafeArrayUtil.sum(numbers)); System.out.println("nullの合計: " + SafeArrayUtil.sum(null)); // 0 } }

まとめ

この章では、Javaの 例外処理 について学んだ。

学んだ内容

  • 例外 はプログラム実行中に発生する予期しない問題である
  • try-catch で例外を捕捉し、適切に処理できる
    • tryブロック:例外が発生する可能性のあるコード
    • catchブロック:例外が発生した時の処理
  • 複数のcatchブロックで異なる例外を処理できる
    • より具体的な例外を先に書く
  • finally は例外の有無に関わらず必ず実行される
    • リソースの解放に使う
  • throwsキーワード でメソッドが例外をスローすることを宣言できる
    • 例外処理の責任を呼び出し側に委譲する
  • throwキーワード で意図的に例外をスローできる
    • 入力値の検証などに使う
  • 検査例外 はコンパイル時にチェックされ、処理が必須である
    • 外部要因で発生する(ファイル、ネットワーク、データベース)
  • 非検査例外 は実行時に発生し、処理は任意である
    • プログラムのバグで発生する(null参照、配列の範囲外)
  • 例外処理により、エラーが発生してもプログラムを適切に制御できる

次のステップ

次の章では、 オブジェクト指向の基礎 について学ぶ。 クラスとオブジェクトの概念を理解し、より実用的で保守性の高いプログラムを作る方法を学ぶ。


FAQ

Q1: ExceptionとErrorの違いは?

A: Javaには Throwable の下に ExceptionError の2系統がある。

ExceptionError
プログラムで対処可能なエラープログラムで対処不可能なエラー
ファイルが見つからない、0で割るメモリ不足、スタックオーバーフロー
try-catchで処理すべき通常は処理しない

Errorの例:

  • OutOfMemoryError:メモリ不足
  • StackOverflowError:スタックオーバーフロー
  • VirtualMachineError:JVM内部エラー

Errorは システムレベルの深刻な問題 であり、通常はプログラムで回復できない。

Exceptionは アプリケーションレベルの問題 であり、適切に処理すれば回復できる。


Q2: 例外を握りつぶしてはいけないのはなぜ?

A: 例外を握りつぶす(catch して何もしない)と、以下の問題が発生する。

悪い例:

try {
// 重要な処理
} catch (Exception e) {
// 何もしない → 例外を握りつぶしている
}

問題点:

  1. エラーが隠蔽される

    • エラーが発生したことに気づけない
    • バグの発見が遅れる
  2. デバッグが困難になる

    • エラーの原因がわからない
    • ログが残らない
  3. データの不整合が発生する

    • 処理が中途半端な状態で終わる
    • 後続の処理に影響する

良い例:

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: はい、影響する。 ただし、適切に使えば問題ない。

例外処理のコスト:

  1. 例外をスローするコスト

    • スタックトレースの生成に時間がかかる
    • 通常の処理の 100〜1000倍 遅い
  2. 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]);
}

例外処理を使うべき場合:

  • 予期しないエラー が発生する可能性がある場合
  • 外部リソース を扱う場合(ファイル、ネットワーク、データベース)
  • 回復可能なエラー を処理する場合

例外処理を使うべきでない場合:

  • 通常の制御フロー を実装する場合
  • 予測可能なエラー を処理する場合(事前チェックで回避できる)
  • パフォーマンスが重要 な処理で頻繁に例外が発生する場合

結論: 例外処理は「例外的な状況」にのみ使い、通常の制御フローには使わない。


演習

Javaの例外(Exception)の説明として最も適切なものを選べ。

正解

D. プログラム実行中に発生するエラーや異常事態のこと

解説

例外(Exception)とは、プログラム実行中 に発生するエラーや異常事態のことである。例えば、0で割り算をしたとき(ArithmeticException)や、配列の範囲外にアクセスしたとき(ArrayIndexOutOfBoundsException)に発生する。コンパイルエラーとは異なり、プログラムを実行して初めて発生する。

try-catch-finallyの各ブロックの役割について、正しい説明を選べ。

正解

D. try: 例外が起きる可能性のある処理、catch: 例外発生時の処理、finally: 例外の有無に関わらず必ず実行する処理

解説
  • try: 例外が発生する可能性のある処理を記述する
  • catch: 例外が発生した場合に実行される処理を記述する
  • finally: 例外の発生有無に関わらず 必ず実行 される処理を記述する(省略可能)

finallyはリソースの解放(ファイルを閉じる等)に使われることが多い。

throwthrows の違いについて、正しい説明を選べ。

throw は例外を「投げる」(発生させる)動作、throws はメソッドが「投げる可能性がある」ことの宣言である。使う場所が異なる点に注目しよう。

正解

D. throwは例外を発生させる文、throwsはメソッド宣言で例外を投げる可能性を示すキーワード

解説
  • throw: メソッド内で例外オブジェクトを 発生させる(例: throw new IllegalArgumentException("不正な値");
  • throws: メソッドの 宣言部分 に書いて、そのメソッドが例外を投げる可能性があることを示す(例: public void read() throws IOException

throw は動作(例外を投げる)、throws は宣言(例外を投げるかもしれない)という違いがある。

以下のコードの空欄を埋めて、0での除算エラーを捕まえるプログラムを完成させよ。

int a = 10; int b = 0;
{ int result = a / b; System.out.println(result); }
{ System.out.println("エラー: 0で割ることはできません"); }

解答例
try / catch (ArithmeticException e)
解説

try ブロックに例外が起きる可能性のある処理を記述し、catch ブロックで例外を捕捉して処理する。catch の引数には例外の型と変数名を指定する。0での除算は ArithmeticException が発生する。

以下のプログラムを完成させよ。

要件

  • ArrayIndexOutOfBoundsExceptionArithmeticException をそれぞれ個別に処理する
  • それ以外の例外は Exception で捕捉する
  • 各catchブロックで適切なメッセージを表示する
public class Main { public static void main(String[] args) { // ここにtry-catch文を書いてください // tryブロック内で配列の範囲外アクセスと0除算を試す } }

catchブロックは複数書くことができる。具体的な例外型を先に、汎用的な例外型を後に 書く。Exception は最も汎用的な例外クラスである。

解説

解答例

try {
    int[] arr = {1, 2, 3};
    System.out.println(arr[5]);
    int result = 10 / 0;
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("配列の範囲外です");
} catch (ArithmeticException e) {
    System.out.println("計算エラーです");
} catch (Exception e) {
    System.out.println("予期しないエラーです: " + e.getMessage());
}

複数のcatchブロックを使うことで、例外の種類に応じた処理ができる。具体的な例外型を先に 書かないとコンパイルエラーになる。

以下のコードの空欄を埋めて、例外の有無に関わらず「処理終了」と表示するプログラムを完成させよ。

try { int result = 10 / 2; System.out.println("結果: " + result); } catch (ArithmeticException e) { System.out.println("エラー発生"); }
{ System.out.println("処理終了"); }

finally ブロックは、例外が発生してもしなくても 必ず 実行される。リソースの後片付け処理に適している。

解答例
finally
解説

finally ブロックは、try-catchの後に記述し、例外の有無に関わらず 必ず実行 される。データベース接続やファイルのクローズなど、確実に実行したい後処理に使用する。

メソッド宣言の throws キーワードの役割として正しいものを選べ。

throws をメソッド宣言に付けると、そのメソッド内で発生した例外を自分では処理せず、呼び出し元に処理を委ねる。

正解

D. メソッド内で例外をキャッチせず、呼び出し元に処理を委ねることを宣言する

解説

throws はメソッド宣言に付けて、そのメソッドが 例外を投げる可能性がある ことを示す。メソッド内でtry-catchで処理する代わりに、呼び出し元に例外処理を任せる。呼び出し元はtry-catchで処理するか、さらにthrowsで上位に委譲する必要がある。

以下のコードの空欄を埋めて、年齢が0未満の場合に例外を発生させるメソッドを完成させよ。

public static void setAge(int age) { if (age < 0) {
; } System.out.println("年齢: " + age); }

throw new 例外クラス("メッセージ") で例外を発生させられる。不正な引数に対して IllegalArgumentException を投げるのが一般的である。

解答例
throw new IllegalArgumentException("年齢は0以上である必要があります")
解説

throw 文を使うと、自分で例外を発生させることができる。不正な引数が渡された場合に IllegalArgumentException を投げるのはよくあるパターンである。throw new 例外クラス("メッセージ") のように new で例外オブジェクトを作成して投げる。

以下のJavaコードを実行した場合、何が出力されるか。

try {
System.out.println("開始");
int result = 10 / 0;
System.out.println("結果: " + result);
} catch (ArithmeticException e) {
System.out.println("エラー発生");
} finally {
System.out.println("終了");
}

tryブロック内で例外が発生すると、その行以降のtryブロック内の処理は スキップ され、対応するcatchブロックに移る。finallyは必ず実行される。

正解

B. 開始 → エラー発生 → 終了

解説

コードの実行の流れ:

  1. System.out.println("開始")「開始」が出力される
  2. int result = 10 / 0;ArithmeticExceptionが発生
  3. System.out.println("結果: " + result);スキップされる(例外発生でtryの残りは実行されない)
  4. catchブロック → 「エラー発生」が出力される
  5. finallyブロック → 「終了」が出力される

例外が発生すると、tryブロック内のそれ以降のコードはスキップされる点がポイントである。

以下のプログラムを完成させよ。

要件

  • ファイル「sample.txt」を読み込んで内容を表示する
  • ファイルが見つからない場合は FileNotFoundException を捕捉してメッセージを表示する
  • 読み込みエラーが発生した場合は IOException を捕捉してメッセージを表示する
import java.io.*; public class Main { public static void main(String[] args) { // ここにファイル読み込みの例外処理を書いてください } }

ファイル操作では FileNotFoundExceptionIOException が発生する可能性がある。BufferedReader を使ってファイルを読む場合、必ずtry-catchで囲む必要がある。finally でリソースを閉じるか、try-with-resources文を使うとよい。

解説

解答例(try-with-resources文を使用):

try (BufferedReader reader = new BufferedReader(new FileReader("sample.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (FileNotFoundException e) {
    System.out.println("ファイルが見つかりません: " + e.getMessage());
} catch (IOException e) {
    System.out.println("読み込みエラー: " + e.getMessage());
}

try-with-resources文(try (リソース) { })を使うと、tryブロック終了時にリソースが 自動的に閉じられるBufferedReaderFileReaderAutoCloseable インターフェースを実装しているため、この構文が使える。