Java講座: 例外処理
これからみなさんが開発をするようになると、「プログラムは正しいはずなのになぜかエラーが発生する」というような状況が起こることがあるかもしれません。
そのような場合に、エラーの内容をログとして保存したり、安全にプログラムを終了させたりできるようにする仕組みを例外処理といいます。
例外とは?
例外とはプログラムを実行する際に発生するエラーのことです。JavaではこれをExceptionとして扱うことができます。例えばこんな例外があります。
- 存在しないファイルを開こうとした(
FileNotFoundException) - 0除算をしようとした(
ArithmeticException) - 中身が
nullの変数にアクセスしようとした(NullPointerException)
例外処理をする理由
例外処理をしなかった場合、エラーが発生した瞬間にプログラムが強制終了してしまいます。しかし、例外処理によってこの挙動はある程度制御することができます。 たとえば、エラーが発生しても安全に次の処理へ進めたり、わかりやすいエラーメッセージを表示したり、エラーの原因をログに記録して修正しやすくしたりできます。
例外処理の基本構文(try-catch-finally)
例外処理の8割はこの構文で処理できるといっても過言です。基本はtry、catch、finallyの3つのブロックを使って処理します。 tryブロック内にエラーが発生する可能性があるコードを、catchブロック内にエラー発生時の処理を、finallyブロック内にはエラーの有無に関わらず最後に必ず実行したい処理を記述します。また、finallyブロックは省略可能です。
例外はこの構文で例外処理を行わないとコンパイルエラーとなり、そもそもプログラムを実行することができません。
サンプルコード
例外処理の例として、「数値が書かれたテキストファイル(input.txt)を読み込み、テキストを数値(int)に変換して出力する」プログラムを添付します。ファイルの読み込みに使用するBufferedReaderはIOExceptionを、FileReaderはFileNotFoundExceptionを引き起こす可能性があるため、try-catch構文でキャッチします。
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
String path = "input.txt";
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line = "";
while ((line = br.readLine()) != null) {
int lineInt = Integer.parseInt(line);
System.out.println(lineInt);
}
} catch (FileNotFoundException e) {
System.err.println("エラー: " + path + "が存在しません");
System.out.println(e.getMessage());
} catch (IOException e) {
e.printStackTrace();
}
}
}同じディレクトリにinput.txtがない状態でこのプログラムを実行してみてください。おそらく
エラー: input.txtが存在しません
input.txt (No such file or directory)みたいなメッセージが出力されるはずです。
では、input.txtに以下のように書いてみたらどうでしょう?
10
20
30
ABC実行してみるとおそらく途中でエラーが発生すると思います。
Exception in thread "main" java.lang.NumberFormatException: For input string: "ABC"
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
at java.base/java.lang.Integer.parseInt(Integer.java:565)
at java.base/java.lang.Integer.parseInt(Integer.java:662)
at Main.main(Main.java:14)ここで「あれ?」と思った人はセンスがあります。
重要なのはエラーメッセージにあるjava.lang.NumberFormatExceptionという文章です。 さきほど「例外はtry-catch構文でキャッチしないとコンパイルエラーとなる」と説明しましたが、例外であるはずのNumberFormatExceptionは今回のコードではキャッチしていません。それなのに普通にコンパイルできてしまいました。これにはちゃんとした理由があります。
例外の種類
Javaのエラー・例外には大きく分けて3つの種類があります。
1つ目は 検査例外(Exception) です。IOExceptionやSQLExceptionなどが当てはまり、try-catch構文で例外処理を行わないとコンパイルエラーとなります。
2つ目は 非検査例外(RuntimeException) です。検査例外と異なり、例外処理を行う必要はありません。NumberFormatExceptionやIllegalArgumentException、NullPointerExceptionなどが当てはまります。先程NumberFormatExceptionをキャッチしていないのにコンパイルができたのはそれが理由だったわけですね。
3つ目は エラー(Error) です。これは「もうプログラムではどうしようもない致命的な事態」が発生したときに起こり、StackOverflowErrorやOutOfMemoryErrorなどが当てはまります。これはもうどうしようもないものなので例外処理を行う必要はありません。
ここまでは、例外が発生した「その場」でtry-catchを使って対処する方法を解説してきました。しかし、実際の開発では「ここではエラーの処理をしたくない(できない)から、このメソッドを呼び出した人に任せよう」というケースが登場します。
これを実現するのがthrowsというキーワードを使った例外処理です。
throws句
throws句は「例外処理を呼び出し元に移譲(丸投げ)」させるためのキーワードです。これにより、throws句を書いたメソッド内ではcatchによる例外処理を行わなくて良くなります。色んな場所から呼び出されるようなメソッドにつける事が多いです。
書き方はメソッド名の右に以下の構文を書けばOKです。
throws [例外名]サンプルコード
さっき紹介したサンプルコードの、「ファイルを読み込む処理」をreadFileメソッドとして切り出してみます。
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
String path = "input.txt";
// 呼び出し元(mainメソッド)がtry-catchで処理する
try {
readFile(path);
} catch (FileNotFoundException e) {
System.err.println("エラー: " + path + "が存在しません");
} catch (IOException e) {
e.printStackTrace();
}
}
// ファイルを読み込むメソッド
// 発生した検査例外はここで処理せず、throwsで呼び出し元(main)へ投げる
public static void readFile(String path) throws FileNotFoundException, IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line = "";
while ((line = br.readLine()) != null) {
int lineInt = Integer.parseInt(line);
System.out.println(lineInt);
}
}
}
}readFileメソッドではBufferedReaderを使用しているため本来ならcatchによって例外をキャッチしなければいけませんが、throws句を書くことによって呼び出し元に例外処理を丸投げしています。その結果、呼び出し元であるmainメソッドではtry-catch構文によって例外処理をしなければいけなくなりました。
try-catchとthrowsの使い分け方
「結局どちらを使えばいいの?」と思うかもしれませんが、基本的にはメソッドの役割によって使い分けます。
呼び出される側のメソッドは
throwsを使う
ファイルを読み込むメソッドやデータベースに接続するメソッドなど、色々な場所から呼ばれる「部品」としてのメソッドはthrowsを使います。部品の内部で勝手に「エラー画面を表示する」などの処理をしてしまうと、別の用途で使い回しにくくなるためです。純粋に決められた処理だけを行い、問題が起きたら呼び出し元に報告するだけにとどめます。プログラム全体を制御する大元のメソッドは
try-catchを使う
mainメソッドや、ユーザーの画面入力を直接受け付けるような大元の処理では、最終的に例外をキャッチします。ここで「ユーザーにわかりやすいエラーメッセージを表示する」「ログファイルに記録して安全に終了する」といった最終判断を下します。
このようにthrowsを活用して「エラーの発生源」と「エラーの最終的な処理」を分離することで、役割分担がはっきりした、整理されたプログラムを作ることができるようになります。