【Java入門】第19回 例外の利用|例外のスロー(throw)とtry・catch・finally
2024.08.02
Javaのプログラムの実行エラー時に発生する例外(Exception)は、エラーの発生をメソッドの呼び出し元に伝えたり、エラーとなった原因の調査や判定に利用するなど、プログラムを作成する上で非常に重要な役割を持つオブジェクトです。
当記事では、例外を利用してエラーの発生を伝達したり、発生した例外に応じて処理を行うためにtry-catch文を利用する方法について説明します。
目次
例外のスロー
例外のスロー(throw)
前回の記事(第18回 Javaのエラー処理)では、Javaのプログラムの実行時に例外が発生した場合、それ以降の処理は途中で中断されると説明しました。
このとき、例外が発生したことを、さらに呼び出し側のプログラム(メソッド)へ伝達する方法が、例外のスロー(throw)です。
下記の例では、要素数が0個のListに対して、1つ目の要素を取得しようとしたため、IndexOutOfBoundsExceptionが発生します。
(26行目「String str = list.get(0);」の部分)
package lesson19;
import java.util.ArrayList;
import java.util.List;
public class ThrowMain {
/** mainメソッド */
public static void main(String[] args) {
System.out.println("処理を開始します。");
List<String> list = new ArrayList<String>() ;
// listの先頭に格納されている文字を取得
String first = getFirstStr(list);
System.out.println("listの先頭の文字列は" + first + "です。") ;
}
/** Listの先頭に格納された文字列を返却する */
public static String getFirstStr(List<String> list) {
System.out.println("Listの先頭の文字列を取得します。");
String str = list.get(0);
// List の先頭の文字列がnull だったかどうかを出力する
if(str == null) {
System.out.println("nullです。");
}else {
System.out.println("nullではありません。");
}
return str;
}
}
実行結果:
処理を開始します。
Listの先頭の文字列を取得します。
Exception in thread "main" java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:100)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302)
at java.base/java.util.Objects.checkIndex(Objects.java:385)
at java.base/java.util.ArrayList.get(ArrayList.java:427)
at lesson19.ThrowMain.getFirstStr(ThrowMain.java:26)
at lesson19.ThrowMain.main(ThrowMain.java:16)
この時、メソッドgetFirstStrから、その呼び出し箇所(16行目「String first = getFirstStr(list);」)のところへ、例外がスローされてきます。
例外を受け取ったメソッド側でこの例外に対して何も措置を行わない場合、受け取った側のメソッドも処置を行わず、上位のメソッドに例外がスローされます。
(上記の例ではmainメソッドで処理を行わないため、プログラムの実行全体がその時点で終了します)
throws句
上記のIndexOutOfBoundsExceptionのように、非検査例外(RuntimeExceptionのサブクラス)発生時には、特別な記述をしなくても例外の上位のメソッド(呼び出し元のメソッド)へ例外がスローされます。
対して、それら以外の検査例外が発生し得るメソッドを実行し、例外発生時に呼び出し元へスローする場合は、メソッドの宣言時にスローする例外の種類を記述(throws宣言)する必要があります。
※検査例外が発生する可能性があるにもかかわらずthrows宣言を行わない場合はコンパイルエラーになります。
throws宣言は、以下の形式で記述します。
(通常のメソッド宣言の最後に、throws句を用いてスローする例外の型を追加)
型 メソッド名 ( 引数 ) throws 発生する例外の型
このとき、複数種類の例外をスローする可能性がある場合は、例外のクラス名をカンマで連結して記述します。
下記の例では、IOExceptionをスローするメソッド「getFileContent 」を、以下のように宣言しています。
String getFileContent ( String filePath ) throws IOException
このように宣言することで、getFileContentメソッドはIOExceptionとそのサブクラスをスローしてくる可能性があるメソッドであると認識されます。
package lesson19;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Collectors;
public class ThrowsMain {
/** mainメソッド */
public static void main(String[] args) throws IOException {
System.out.println("処理を開始します。");
String fileName = "nothing.txt"; // 例外を発生させるため、存在しないファイルのパスを指定
String fileContent = getFileContent(fileName);
// 取得したファイルの内容を出力
System.out.println(fileName + "の内容:" + fileContent);
System.out.println("処理を終了します。");
}
/** 指定したパスのファイルの内容を取得して返却する */
public static String getFileContent(String filePath) throws IOException {
String content = null;
Path path = Paths.get(filePath);
// ファイルの全行を読み込み(改行コードで接続)
content = Files.lines(path, StandardCharsets.UTF_8).collect(Collectors.joining("\r\n"));
return content;
}
}
実行結果:
Exception in thread "main" java.nio.file.NoSuchFileException: nothing.txt
at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:119)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:309)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:369)
at java.base/java.nio.file.Files.lines(Files.java:4112)
at lesson19.ThrowsMain.getFileContent(ThrowsMain.java:36)
at lesson19.ThrowsMain.main(ThrowsMain.java:19)
この例ではmainメソッドでもスローされてきたIOExceptionに対して措置を行わないため、mainメソッドにもthrows宣言を追加していますが、実際のアプリケーションでは、これらの例外に対してメソッドの階層のどこかで措置を行うことが基本です。
スローされてきた例外に対して措置を行う方法としては、次に述べる「try-catch文」を利用します。
try-catch文
try-catch
try-catch文は、呼び出したメソッドからスローされてきた例外を検知し、例外に対する処理を行うための構文で、以下の形式で記述します。
try { // 例外がスローされる可能性のある処理 } catch ( 例外の型 変数名 ) { // 例外検知時の処理 }
try から catch までの間のブロックを「tryブロック」、catch句に続くブロックを「catchブロック」と呼びます。
throws句の説明で利用したサンプルコードのmainメソッドを以下のように変更して、実際に利用してみましょう。
/** mainメソッド */
public static void main(String[] args) {
System.out.println("処理を開始します。");
String fileName = "nothing.txt"; // 例外を発生させるため、存在しないファイルのパスを指定
try {
String fileContent = getFileContent(fileName);
// 取得したファイルの内容を出力
System.out.println(fileName + "の内容:" + fileContent);
} catch(IOException ex) {
// 例外(IOException)の発生を検知
System.out.println("ファイルの読み込みで例外が発生しました。");
System.out.println("ファイル名:" + fileName);
// 例外のスタックトレースを出力
ex.printStackTrace();
}
System.out.println("処理を終了します。");
}
実行結果:
処理を開始します。
ファイルの読み込みで例外が発生しました。
ファイル名:nothing.txt
java.nio.file.NoSuchFileException: nothing.txt
at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:119)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:309)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:369)
at java.base/java.nio.file.Files.lines(Files.java:4112)
at lesson19.ThrowsMain.getFileContent(ThrowsMain.java:45)
at lesson19.ThrowsMain.main(ThrowsMain.java:20)
処理を終了します。
catchブロックでは、検知する対象のクラス名を指定し、その例外が発生した場合の処理を記述します。(tryブロック内の処理から複数種類の例外がスローされる場合は、複数個のcatchブロックを連結して記述します)
上記の例では、mainメソッドの中で呼び出した「getFileContent」メソッドからスローされてきた「NoSuchFileException」(IOExceptionのサブクラス)を検知し、エラーが発生したことを通知するメッセージやファイル名、例外のスタックトレースを出力しています。
この時、tryブロック内の例外の発生地点からcatchブロックまでの間の処理(「取得したファイルの内容を出力」の部分の処理)はスキップされることに注意してください。
また、例外をcatchしてさらに上位へスローしない場合、スタックトレースは自動的に出力されません。
(上記の例では「ex.printStackTrace」メソッドを呼び出してスタックトレースを出力しています)
catchブロックは、発生し得る例外の種類に応じて複数個設定することも可能です。ただし、並列に並べた場合は最初に条件が合致したcatchブロックのみ処理されることに注意してください。
以下の例では、IOException、NumberFormatException、その他の例外が発生した場合に、別の処理を行います。
try {
// IOException、NumberFormatException、その他の例外をthrowする処理を呼出
anyMethod();
} catch (IOException ex) {
// IOException発生時の処理
} catch (NumberFormatException ex) {
// NumberFormatException発生時の処理
} catch (Exception ex) {
// IOException、NumberFormatException以外の例外発生時の処理
}
このとき、catchブロックの記述順によっては、到達することがないcatchブロックとなることに注意してください。
以下の例では、最初のcatchブロック(「Exception」に対するcatchブロック)が、全ての例外発生をcatchしてしまうため、後続のcatchブロックに到達することがなくなってしまいます。
try {
// IOException、NumberFormatException、その他の例外をthrowする処理を呼出
anyMethod();
} catch (Exception ex) {
// 全てのException発生時の処理
} catch (IOException ex) {
// 最初のブロックでIOExceptionもcatchされるため、ここには到達しない
} catch (NumberFormatException ex) {
// 最初のブロックでNumberFormatExceptionもcatchされるため、ここには到達しない
}
try-catch-finally
try-catch文では、finallyブロックを追加することで、例外の発生有無に関わらずtryブロックの処理後に必ず実行する処理を記述することができます。
下記の例では、catchブロックで検知した例外を再度上位のメソッドへ再度スローしています。
(「throw ex;」 の部分)
通常、例外をスローした場合は後続の処理が実行されませんが、try-catch文に連結してfinallyブロックがある場合は、finallyブロック内の処理(下記の例では「処理を終了します。」を出力)だけは、必ず実行されます。
/** mainメソッド */
public static void main(String[] args) throws IOException{
System.out.println("処理を開始します。");
String fileName = "nothing.txt"; // 例外を発生させるため、存在しないファイルのパスを指定
try {
String fileContent = getFileContent(fileName);
// 取得したファイルの内容を出力
System.out.println(fileName + "の内容:" + fileContent);
} catch(IOException ex) {
// 例外(IOException)の発生を検知
System.out.println("ファイルの読み込みで例外が発生しました。");
System.out.println("ファイル名:" + fileName);
throw ex;
} finally {
System.out.println("処理を終了します。");
}
}
実行結果:
処理を開始します。
ファイルの読み込みで例外が発生しました。
ファイル名:nothing.txt
処理を終了します。
Exception in thread "main" java.nio.file.NoSuchFileException: nothing.txt
at java.base/sun.nio.fs.WindowsException.translateToIOException(WindowsException.java:85)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:103)
at java.base/sun.nio.fs.WindowsException.rethrowAsIOException(WindowsException.java:108)
at java.base/sun.nio.fs.WindowsFileSystemProvider.newFileChannel(WindowsFileSystemProvider.java:119)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:309)
at java.base/java.nio.channels.FileChannel.open(FileChannel.java:369)
at java.base/java.nio.file.Files.lines(Files.java:4112)
at lesson19.ThrowsMain.getFileContent(ThrowsMain.java:45)
at lesson19.ThrowsMain.main(ThrowsMain.java:20)
finallyブロックの処理は、例外が発生しなかった場合もtryブロック内の処理に続けて実行されます。
試しに、以下の内容のファイルを作成して「C:\java\exists.txt」として保存し、NoSuchFileExceptionが発生しない(正しくファイルが読み込める)ようにコードを変更して実行してみます。
/** mainメソッド */
public static void main(String[] args) throws IOException{
System.out.println("処理を開始します。");
String fileName = "C:/java/exists.txt"; // 実際に作成したファイルを指定
try {
String fileContent = getFileContent(fileName);
// 取得したファイルの内容を出力
System.out.println(fileName + "の内容:" + fileContent);
} catch(IOException ex) {
// 例外(IOException)の発生を検知
System.out.println("ファイルの読み込みで例外が発生しました。");
System.out.println("ファイル名:" + fileName);
throw ex;
} finally {
System.out.println("処理を終了します。");
}
}
実行結果:
処理を開始します。
C:/java/exists.txtの内容:このファイルは存在します。
行数は2行です。
処理を終了します。
実行結果から、例外が発生しない場合も(catchブロックの処理が実行されずに)finallyブロック内の処理が実行されていることがわかります。
catchブロックを複数個記述した場合でも、finallyブロックは最後に1つだけ記述することができ、どのcatchブロックの処理が行われても(または、どのcatchブロックの処理も行われなくても)、必ず同じ処理が実行されます。
例外のハンドリング
try-catchとthrowsの使い分け
例外が発生した場合は try-catch文で検知して処置すべきか、throws宣言を行ってそのまま例外をスローすべきか、どのように判断すべきでしょうか。
これについては、絶対的なルールは存在していません。作成するアプリケーションの考え方などから、部品用のクラス、メソッドごとのルールなどを決めることが多いですが、これも必ずしも絶対とはいえません。
特にプロジェクトなどでルールが無い場合、一つの目安として筆者は以下の内容を基本的な考え方としています。
・メソッド自身の仕様として例外発生時の処理が決まっている場合は、メソッド内でcatchしてハンドリングを行う。
・メソッド内だけでは判断できず、呼び出し元の処理に応じてハンドリングしてもらう必要がある場合はcatchせずにthrows宣言を行う(または、catchしても再度スローする)。
例えば、以下のサンプルメソッドは数値形式の文字列("123"、"-1,234"など)を数値化して返却するメソッドですが、文字列が数値形式ではなかった("八十八"、"日"など)場合はintの最小値を返す、という仕様が決まっているメソッドとなっています。
このため、このメソッドではInteger.parseIntメソッドから返される例外を検知して、Integerの最小値を返すようにしています。
/** 引数の文字列の数値としての値を返却する */
public int getInt(String str) {
// デフォルト返却値を0とする
int result = 0;
// null または空白の場合はデフォルト値を返却
if(str == null || str.isBlank()) {
return result;
}
try {
// Integer.parseInt を利用して数値化する
result = Integer.parseInt(str);
} catch(NumberFormatException ex) {
// 数値型の文字列ではなかった場合はIntegerの最小値を返却
result = Integer.MIN_VALUE;
}
return result;
}
ただし、筆者も共通部品やアプリケーション基盤と呼ばれるような、特定の役割を持つプログラムを作成する場合は、また考え方が違います。
皆さんも、どのように例外をハンドリングすべきか、色々試してみるのも面白いと思いますので、是非チャレンジしてみてください。