Skip to content
Playground

9.例外処理

学習目標

  • 例外が発生したときのプログラムの挙動を、スタックトレースから読み取れる
  • try-catch-finally で例外を捕まえ、処理を続けられる
  • throw で例外を投げ、throws で呼び出し元に伝えられる
  • チェック例外と非チェック例外を、エラーの性質から使い分けられる
  • try-with-resources でリソースを自動で閉じられる

プログラムの実行中に発生するエラーを 例外 と呼びます。たとえば、要素が 3 つの配列に対して存在しない添字 5 の要素にアクセスすると、ArrayIndexOutOfBoundsException という例外が発生します。

Main.java
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 つから調べます。

例外が発生しても、その時点で異常終了させず、エラーに対処してから先へ進める仕組みが try-catch です。try ブロックに例外が発生する可能性のある処理を書き、catch ブロックに例外が発生したときの処理を書きます。

try {
例外が発生する可能性のある処理
} catch (例外の型 変数名) {
例外が発生したときに実行する処理
}

範囲外アクセスのコードを try-catch で囲みます。

Main.java
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 3

numbers[5] で例外が発生すると、その時点で catch ブロックへ処理が移り、エラーメッセージを出力します。例外を捕まえたため、プログラムは異常終了しません。catch で受け取った変数 e は発生した例外のインスタンスで、e.getMessage() でエラーメッセージを取得できます。

ただし範囲外アクセスは、添字の指定が誤っているという、コードを直せば防げるエラーです。本来は numbers[5] を正しい添字に直すべきで、try-catch で捕まえる対象ではありません。ここでは構文を示すために使っています。try-catch で対処する対象は、ファイルの読み込みのように、正しいコードでも実行時の状況で失敗する処理です。その区別は チェック例外と非チェック例外 で扱います。

例外が起きても起きなくても、必ず実行したい終了処理があります。try ブロックが正常に終わった場合と、catch に移った場合の両方に同じ処理を書くと、コードが重複し、片方への書き忘れも生じます。finally ブロックは、どちらの場合も必ず実行されます。

Main.java
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 は、ファイルやネットワーク接続などのリソースを、例外の有無にかかわらず確実に閉じる用途で使います。

データクラスsetAge は、不正な年齢を println で表示して return で中断していました。この書き方では、メッセージは出るものの、呼び出し元は失敗を知らないまま次の処理へ進みます。失敗を呼び出し元に確実に伝えるには、setAge 自身が例外を投げます。

例外は標準ライブラリが投げるものだけでなく、自分のメソッドからも投げられます。例外を投げるには throw 文を使います。

throw new 例外の型(メッセージ);

setAge を、IllegalArgumentException を投げる形に変えます。IllegalArgumentException は「不正な引数」を表す標準例外です。

Person.java
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("不正な年齢: " + age);
}
this.age = age;
}

Main から不正な年齢を渡すと、setAge の中で例外が投げられます。

Main.java
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. チェック例外と非チェック例外”

setAgeIllegalArgumentException は、try-catch で捕まえなくてもコンパイルできました。一方、try-catch で捕まえるか throws で宣言しないとコンパイルが通らない例外もあります。前者を 非チェック例外、後者を チェック例外 と呼び、Java の例外はこの 2 つに分かれます。

種類エラーの性質コンパイラーの扱い
非チェック例外コードを直せば防げる対処を強制しないIllegalArgumentException
NullPointerException
チェック例外実行時に避けられないtry-catchthrows が必須IOException
SQLException

チェック例外を投げるメソッドを呼ぶ側は、try-catch で対処するか、throws で呼び出し元に伝えるかを選びます。

チェック例外を投げるメソッドの例として、ファイルの全行を読み込む Files.readAllLines を使います。このメソッドは、ファイルが存在しないなどの理由で IOException(チェック例外)を投げます。

IOException をメソッドの中で処理しない場合は、throws を付けて呼び出し元に伝えます。

FileReader.java
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));
}
}

readLinesthrows IOException を宣言しているため、これを呼ぶ側が try-catch で対処します。

Main.java
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 でも変わりません。

ファイルやキーボード入力などのリソースは、使い終わったら閉じる必要があります。try-with-resources は、リソースの解放を自動で行う構文です。

try (リソースの宣言) {
リソースを使う処理
}

キーボードからの入力を受け取る Scanner も、使い終わったら閉じる必要のあるリソースです。

Main.java
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 で明示的に閉じるコードを書く必要はありません。

例外を捕まえても、catch の中身を誤ると、捕まえたエラーが表に出ず、問題が見過ごされます。

ファイルを読み込み、その行数を表示するコードで見ます。catch ブロックを空にすると、読み込みに失敗しても何も起きません。

Main.java
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 が存在しない場合、readAllLinesIOException を投げます。空の catch がこれを捕まえますが、何もしないため lines は空のままです。

読み込んだ行数: 0

読み込みは失敗したのに、エラーは表に出ず、読み込んだ行数: 0 が出力されます。後続の処理は「0 行のファイルを読み込めた」前提で進み、失敗が見過ごされます。catch で例外を捕まえながら何もしないこの書き方を、例外を握りつぶすと言います。

握りつぶしを避けるには、エラーを記録し、失敗したあとの処理を続けないようにします。エラーの記録には System.err を使います。System.out が通常の出力先であるのに対し、System.err はエラー出力専用で、ログを通常の出力と区別できます。

Main.java
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.txt

catch でエラーを記録し、return で処理を打ち切ります。失敗したまま 読み込んだ行数 の出力へ進むことがなくなります。記録を残すだけでなく、失敗後に処理を続けない点が握りつぶしとの違いです。