9.例外処理
学習目標
- 例外が発生したときのプログラムの挙動を、スタックトレースから読み取れる
try-catch-finallyで例外を捕まえ、処理を続けられるthrowで例外を投げ、throwsで呼び出し元に伝えられる- チェック例外と非チェック例外を、エラーの性質から使い分けられる
- try-with-resources でリソースを自動で閉じられる
1. 例外とプログラムの停止
Section titled “1. 例外とプログラムの停止”プログラムの実行中に発生するエラーを 例外 と呼びます。たとえば、要素が 3 つの配列に対して存在しない添字 5 の要素にアクセスすると、ArrayIndexOutOfBoundsException という例外が発生します。
int[] numbers = {10, 20, 30};
System.out.println(numbers[5]);System.out.println("処理を続けます");Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 3 at Main.main(Main.java:3)例外に対処しないと、プログラムは発生した時点で異常終了します。次の行の System.out.println("処理を続けます") は実行されず、処理を続けます は出力されません。
異常終了すると、上のような スタックトレース が表示されます。スタックトレースは、発生した例外について次の 3 つを示します。
| 行 | 示す内容 |
|---|---|
java.lang.ArrayIndexOutOfBoundsException | 例外の型 |
Index 5 out of bounds for length 3 | 例外のメッセージ(長さ 3 の配列に添字 5 を使った) |
at Main.main(Main.java:3) | 発生場所(Main.java の 3 行目、main メソッド) |
エラーの原因は、この 3 つから調べます。
2. 例外の捕捉
Section titled “2. 例外の捕捉”例外が発生しても、その時点で異常終了させず、エラーに対処してから先へ進める仕組みが try-catch です。try ブロックに例外が発生する可能性のある処理を書き、catch ブロックに例外が発生したときの処理を書きます。
try { 例外が発生する可能性のある処理} catch (例外の型 変数名) { 例外が発生したときに実行する処理}範囲外アクセスのコードを try-catch で囲みます。
int[] numbers = {10, 20, 30};
try { System.out.println(numbers[5]);} catch (ArrayIndexOutOfBoundsException e) { System.out.println("範囲外です: " + e.getMessage());}範囲外です: Index 5 out of bounds for length 3numbers[5] で例外が発生すると、その時点で catch ブロックへ処理が移り、エラーメッセージを出力します。例外を捕まえたため、プログラムは異常終了しません。catch で受け取った変数 e は発生した例外のインスタンスで、e.getMessage() でエラーメッセージを取得できます。
ただし範囲外アクセスは、添字の指定が誤っているという、コードを直せば防げるエラーです。本来は numbers[5] を正しい添字に直すべきで、try-catch で捕まえる対象ではありません。ここでは構文を示すために使っています。try-catch で対処する対象は、ファイルの読み込みのように、正しいコードでも実行時の状況で失敗する処理です。その区別は チェック例外と非チェック例外 で扱います。
2-1. finally ブロック
Section titled “2-1. finally ブロック”例外が起きても起きなくても、必ず実行したい終了処理があります。try ブロックが正常に終わった場合と、catch に移った場合の両方に同じ処理を書くと、コードが重複し、片方への書き忘れも生じます。finally ブロックは、どちらの場合も必ず実行されます。
int[] numbers = {10, 20, 30};
try { System.out.println(numbers[5]);} catch (ArrayIndexOutOfBoundsException e) { System.out.println("範囲外です");} finally { System.out.println("終了処理");}範囲外です終了処理catch の処理に続いて、finally の 終了処理 が出力されます。numbers[5] を numbers[0] に変えて例外が発生しない場合も、finally は実行されます。finally は、ファイルやネットワーク接続などのリソースを、例外の有無にかかわらず確実に閉じる用途で使います。
3. 例外の送出
Section titled “3. 例外の送出”データクラス の setAge は、不正な年齢を println で表示して return で中断していました。この書き方では、メッセージは出るものの、呼び出し元は失敗を知らないまま次の処理へ進みます。失敗を呼び出し元に確実に伝えるには、setAge 自身が例外を投げます。
例外は標準ライブラリが投げるものだけでなく、自分のメソッドからも投げられます。例外を投げるには throw 文を使います。
throw new 例外の型(メッセージ);setAge を、IllegalArgumentException を投げる形に変えます。IllegalArgumentException は「不正な引数」を表す標準例外です。
public void setAge(int age) { if (age < 0) { throw new IllegalArgumentException("不正な年齢: " + age); } this.age = age;}Main から不正な年齢を渡すと、setAge の中で例外が投げられます。
Person p = new Person("田中", 25);p.setAge(-5);System.out.println("年齢を更新しました");Exception in thread "main" java.lang.IllegalArgumentException: 不正な年齢: -5 at Person.setAge(Person.java:3) at Main.main(Main.java:2)setAge(-5) で例外が投げられ、次の行の 年齢を更新しました は実行されず、プログラムは異常終了します。println で表示する書き方では失敗が呼び出し元に伝わりませんでしたが、例外を投げると失敗が Main に伝わり、対処しなければ停止します。失敗したまま後続の処理へ進むことがありません。スタックトレースは setAge で投げられた例外が main まで伝わったことを示します。
ここで setAge(-5) の -5 は、Main のコードが渡した不正な値です。これはコードを直せば防げるエラーなので、try-catch で捕まえるのではなく、setAge に正しい年齢を渡すよう Main を修正します。
4. チェック例外と非チェック例外
Section titled “4. チェック例外と非チェック例外”setAge の IllegalArgumentException は、try-catch で捕まえなくてもコンパイルできました。一方、try-catch で捕まえるか throws で宣言しないとコンパイルが通らない例外もあります。前者を 非チェック例外、後者を チェック例外 と呼び、Java の例外はこの 2 つに分かれます。
| 種類 | エラーの性質 | コンパイラーの扱い | 例 |
|---|---|---|---|
| 非チェック例外 | コードを直せば防げる | 対処を強制しない | IllegalArgumentExceptionNullPointerException |
| チェック例外 | 実行時に避けられない | try-catch か throws が必須 | IOExceptionSQLException |
チェック例外を投げるメソッドを呼ぶ側は、try-catch で対処するか、throws で呼び出し元に伝えるかを選びます。
4-1. throws による伝播
Section titled “4-1. throws による伝播”チェック例外を投げるメソッドの例として、ファイルの全行を読み込む Files.readAllLines を使います。このメソッドは、ファイルが存在しないなどの理由で IOException(チェック例外)を投げます。
IOException をメソッドの中で処理しない場合は、throws を付けて呼び出し元に伝えます。
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.util.List;
public class FileReader { public List<String> readLines(String fileName) throws IOException { return Files.readAllLines(Path.of(fileName)); }}readLines は throws IOException を宣言しているため、これを呼ぶ側が try-catch で対処します。
FileReader reader = new FileReader();
try { List<String> lines = reader.readLines("notes.txt"); System.out.println("行数: " + lines.size());} catch (IOException e) { System.out.println("読み込みに失敗しました: " + e.getMessage());}throws を書くと、メソッドの宣言を見ただけで、そのメソッドがどの例外を投げるかが分かり、呼び出し元は対処を求められます。チェック例外を throws で伝え、呼び出し元が try-catch で対処するこの流れは、データベースを操作するときの SQLException でも変わりません。
5. try-with-resources
Section titled “5. try-with-resources”ファイルやキーボード入力などのリソースは、使い終わったら閉じる必要があります。try-with-resources は、リソースの解放を自動で行う構文です。
try (リソースの宣言) { リソースを使う処理}キーボードからの入力を受け取る Scanner も、使い終わったら閉じる必要のあるリソースです。
import java.util.Scanner;
try (Scanner scanner = new Scanner(System.in)) { System.out.print("名前を入力: "); String name = scanner.nextLine(); System.out.println("こんにちは、" + name + "さん");}try の () の中で宣言した scanner は、try ブロックを抜けるときに自動で閉じられます。例外が発生した場合も閉じられます。finally で明示的に閉じるコードを書く必要はありません。
6. 例外処理の作法
Section titled “6. 例外処理の作法”例外を捕まえても、catch の中身を誤ると、捕まえたエラーが表に出ず、問題が見過ごされます。
ファイルを読み込み、その行数を表示するコードで見ます。catch ブロックを空にすると、読み込みに失敗しても何も起きません。
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.util.List;
List<String> lines = List.of();
try { lines = Files.readAllLines(Path.of("notes.txt"));} catch (IOException e) { // 何もしない}
System.out.println("読み込んだ行数: " + lines.size());notes.txt が存在しない場合、readAllLines は IOException を投げます。空の catch がこれを捕まえますが、何もしないため lines は空のままです。
読み込んだ行数: 0読み込みは失敗したのに、エラーは表に出ず、読み込んだ行数: 0 が出力されます。後続の処理は「0 行のファイルを読み込めた」前提で進み、失敗が見過ごされます。catch で例外を捕まえながら何もしないこの書き方を、例外を握りつぶすと言います。
握りつぶしを避けるには、エラーを記録し、失敗したあとの処理を続けないようにします。エラーの記録には System.err を使います。System.out が通常の出力先であるのに対し、System.err はエラー出力専用で、ログを通常の出力と区別できます。
List<String> lines;
try { lines = Files.readAllLines(Path.of("notes.txt"));} catch (IOException e) { System.err.println("ファイルの読み込みに失敗しました: " + e.getMessage()); return;}
System.out.println("読み込んだ行数: " + lines.size());ファイルの読み込みに失敗しました: notes.txtcatch でエラーを記録し、return で処理を打ち切ります。失敗したまま 読み込んだ行数 の出力へ進むことがなくなります。記録を残すだけでなく、失敗後に処理を続けない点が握りつぶしとの違いです。