【Java入門】第18回 Javaのエラー処理|例外(Exception)とエラー(Error)
2024.07.26
プログラミングにおいては、コンパイル時や実行時など、どのような場面においてエラーは切り離せないものです。当記事では、Javaのプログラミング中に起きるエラーや、Javaの言語仕様としての例外、エラーについて説明します。
コンパイルエラーと実行時エラー
コンパイルエラーは、Javaのプログラムの文法上のエラーで、コンパイル時に検知されます。
(javaコマンドでファイルを直接実行する場合も、内部的にコンパイルする時点で検知されます)
EclipseなどのIDEを利用している場合は、コンパイルエラーがある場合はエディタ上に表示される場合が多いため、容易に修正できると思います。
これに対し実行時エラーは、その名のとおりプログラムの「実行時」に発生するエラーです。
今回は、この実行時エラーである例外(Exception)とエラー(Error)のサブクラス群について説明したいと思います。
例外(Exception)
例外とは
プログラムの実行エラーの発生時に、呼び出し元へ例外クラスを生成して返すことで、プログラム上で検知することが可能とするための仕組みです。
例えば「ファイルを取得しようとしたが存在しなかった」「データベースに接続しようとしたが、認証エラーになった」「単純に値を設定する処理が漏れていた」などのエラーが発生した場合、処理の呼び出し側のプログラムに、エラーの内容に応じた例外が返されることで、エラーの検知やプログラムの修正に役立てることができます。
throwとtry-catch構文
Javaのプログラムでは、エラーの検知時に例外オブジェクトを生成して呼び出し元に渡しますが、この場合は処理を途中で中断するため、通常のメソッドのように「return」句ではなく、「throw(スロー)」句を使って例外オブジェクトを渡します。
throwされた例外の詳細は、次回紹介する「try-catch構文」を利用することで、発生したエラーについてプログラム内での検知と内容の検証を行うことが可能です。
例外オブジェクトには、発生した例外の種類や原因、エラーメッセージなど、エラーの解消のために必要な情報が詰まっているため、プログラムを作成するうえで非常に重要な役割を持ちます。
Exceptionクラス
ごく一部のエラーを除き、Javaのプログラム上で実行時エラーが発生した場合は、「Exception(エクセプション)」というクラスのサブクラスのオブジェクト(例外オブジェクト)が throwされます。
Exceptionクラスのサブクラスは非常に多岐に渡りますが、大きくは「検査例外」と「非検査例外」に分けられ、さらにこれらの中でも実行する処理の分類などによって系統立てられています。
このため、発生した例外の検証時には、そのスーパークラスの情報などからも、おおまかな発生理由を把握することができます。
(参考)Throwableクラス
Throwableクラスは「Java言語のすべてのエラーと例外」のスーパークラスです。
(Exceptionクラスと、後述するErrorクラスの親クラスです)
Javaの文法上、Exceptionとそのサブクラス群を含む、Throwableクラスのサブクラスがthrowの対象であり、try-catch構文で検知できる対象となります。
検査例外と非検査例外
Exceptionのサブクラス群は、大きく「検査例外」と「非検査例外」の2つに分けられます。
検査例外
検査例外は、先ほど例として挙げた「ファイルを取得しようとしたが存在しなかった」「データベースに接続しようとしたが、認証エラーになった」など、ライブラリの利用時などに『その発生を十分に想定して対処を考える必要がある』例外を指します。
これらの検査例外は、発生時にプログラムのコードをなぞるだけでは原因を調査するのが難しいため、発生の可能性があるメソッドを利用する場合は必ず「try-catch構文」でくくり、検知した場合の処理(検査)が求められます。
このように「try-catch構文でくくって検査を行う」必要があるため、これらの例外のことを「検査例外」と呼びます。
IOException
(java.io.IOException)
IOExceptionは、コンピューター上のファイルやフォルダ(ディレクトリ)に対して、読み込み、作成、削除などの操作をしようとした場合に発生する例外です。
以下の例では、14行目で存在しないファイルを開いて読み込もうとしたため、IOException のサブクラスである FileNotFoundException が発生します。
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class FileNotFoundMain {
/** mainメソッド */
public static void main(String[] args) {
File fl = new File("NoSuchFile.txt");
FileReader fr = null;
try {
fr = new FileReader(fl); // ★存在しないファイルを開いて読み込み
} catch(IOException ex) {
System.out.println("ファイル「NoSuchFile.txt」の読み込みに失敗しました");
System.out.println("発生した例外の種類:" + ex.getClass().getName());
System.out.println(ex.getMessage());
}
}
}
実行結果:
ファイル「NoSuchFile.txt」の読み込みに失敗しました
発生した例外の種類:java.io.FileNotFoundException
NoSuchFile.txt (指定されたファイルが見つかりません。)
非検査例外(Runtime Exception)
「プログラム上で必ず値が入っているはずが、処理の呼び出しが漏れていたのでnullのままだった」「設定ファイルに指定する値の桁数を間違えていた」など、プログラム上や単一のメソッド内では必ずしも『常に発生を想定すべき』とまではいえない例外のクラス群です。
非検査例外は全て「RuntimeException」クラスのサブクラスとして定義されます。
コーディングの際は、非検査例外は「try-catch構文」で囲わなくてもコンパイルエラーとはなりません。
NullPointerException
(java.lang.NullPointerException)
NullPointerExceptionは、値が設定されていない(nullである)変数に対し、メソッドなどの呼び出しを行った際に発生する例外です。
単純なミスやロジックの考慮誤りなどで発生する場合がほとんどで、皆さんも最も目にする機会が多い例外だと思います。
以下の例では、nullである変数「str」に対して、「length」メソッドを呼び出してしまったために NullPointerException が発生します。
String str = null;
if(str.length() > 0){ // str.length() nullに対して . で処理を呼び出したためNullPointerExceptionが発生
System.out.println(str);
}
実行結果:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "str" is null
例外の発生時は(内部でハンドリングしない限り)、発生した例外の内容が後述するスタックトレースと一緒に出力されます。
IndexOutOfBoundsException
(java.lang.IndexOutOfBoundsException)
IndexOutOfBoundsExceptionは、配列やListなど長さと位置(Index)を持つオブジェクトに対して、範囲外の位置を指定した場合に発生します。
こちらもプログラム入門時は発生しやすい例外と言えるでしょう。
以下の例では、長さが3(Indexは 0 から 2 の範囲)の配列に対して、不正な位置(-1)を指定したため、IndexOutOfBoundsException のサブクラスであるArrayIndexOutOfBoundsExceptionが発生します。
String[] strArray = {"A", "B", "C"}; // 配列のIndexは0~2
System.out.println(strArray[-1]); // 存在しないIndex(10)にアクセス
実行結果:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
エラー(Error)
Errorクラスとは
Error(エラー)とそのサブクラスは、アプリケーションが続行不能となるような重大な問題を示すクラス群です。
Errorは例外(Exception)同じThrowableのサブクラスですが、Exceptionとは系統が分けられています。
Error の主なサブクラス
比較的容易に発生するErrorのサブクラスとして、以下の2つのエラー「OutOfMemoryError」と「StackOverflowError」を紹介します。
OutOfMemoryError
(java.lang.OutOfMemoryError)
OutOfMemoryErrorは、文字のとおりメモリ(※)が不足してしまい、オブジェクトの生成などができず継続不能となった状態で発生します。
※ここで示す「メモリ」とは、Javaの「ヒープ(Heap)」と呼ばれる、JavaVMが確保しているメモリのサイズです。(コンピューター自体のメモリ容量とは異なります)
無限ループの中でオブジェクトを生成し続けた場合や、大きすぎるファイルを読み込んだ場合など、最も発生しやすいErrorと言えると思います。
StackOverflowError
(java.lang.StackOverflowError)
StackOverflowErrorは、メソッドの呼び出し階層が多すぎる場合に発生します。
※上限回数は環境にも依存しますが、Eclipse上で実行する場合、デフォルトであれば1024回が上限となります。
以下の例では、recurse メソッドを最大10,000,000回再帰呼び出し(メソッドの中で、自分自身のメソッドをさらに呼び出すこと)を行うため、呼び出し階層の上限を超えた時点でStackOverflowErrorが発生します。
public class ExceptionMain {
public static void main(String[] arrs) {
recurse(0);
}
public static void recurse(int i) {
// 10000000に至るまで引数に1加算して自身のメソッドを再帰的に呼び出す
if( i < 10000000) {
recurse(i + 1);
}
}
}
実行結果:
Exception in thread "main" java.lang.StackOverflowError
Exceptionとの違い
先述のとおり、続行不能となるような重大な問題の場合を示すため、Exceptionとは系統が分けられたクラス群となっています。
Throwableのサブクラスなのでtry-catchは可能ですが、発生原因がコーディングだけに限らず実行環境などに起因することも多く、Errorが発生した場合、一部を除きアプリケーション内での検知は困難なことが多いため、単独のアプリケーション内でハンドリングすることはあまりありません。
スタックトレース
スタックトレースとは
Javaなどのプログラム言語では、プログラムの実行中時に呼び出したメソッドなどの履歴・階層を保持しています。
Javaで例外が発生した際は、try-catchなどを利用してプログラム内でハンドリングを行わなかった場合、スタックトレースとしてこの履歴が出力されます。
スタックトレースの内容
例外のスタックトレースでは、例外が発生した箇所までの処理の呼び出し階層と、その呼び出しがどのコードのどの位置で行われているか、を把握することが可能です。
以下の例では、main ⇒ getResult ⇒ getMessage ⇒ editMessage の順番で、各メソッド内から次のメソッドを呼び出し、最後の editMessage で例外が発生します。
public class ExceptionMain {
/** mainメソッド */
public static void main(String[] arrs) {
String result = getResult();
// 処理結果を出力
System.out.println(result);
}
/** 何らかの処理をして、処理結果のメッセージを返却します*/
public static String getResult() {
String errCode = null;
// ★ 何らかの処理を行い、正しく処理でき鳴った場合はerrCodeに値を設定する。
// 最後に処理結果のメッセージを取得
return getMessage(errCode);
}
/** 処理結果メッセージを編集します */
public static String getMessage(String errCode) {
String msg = editMessage(errCode);
if(msg == null) {
msg = "処理は成功しました。";
}
return msg;
}
/** エラーコードを判断して、エラーメッセージを返却します */
public static String editMessage(String errCode) {
if(errCode.startsWith("5")) { // ★先にnullチェックをしていないためここで例外発生
return "システム内部エラーです。";
}else if(errCode != null){
return "その他のエラーです。";
}else{
return null;
}
}
}
実行結果:
発生した例外(NullPointerException)のメッセージと、スタックトレースが出力されます。
呼び出された直近の方から順にメソッドの呼び出し階層と、そのコードの位置が把握できます。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.startsWith(String)" because "<parameter1>" is null
at ExceptionMain.editMessage(ExceptionMain.java:40)
at ExceptionMain.getMessage(ExceptionMain.java:28)
at ExceptionMain.getResult(ExceptionMain.java:21)
at ExceptionMain.main(ExceptionMain.java:6)
このように、例外のメッセージとその呼び出し階層から、どこにプログラム上の問題があるのかを把握することができます。
例えば上記のサンプルコードでは、以下のようにプログラムの一部(getMessage)を修正することで、このエラーを回避することができます。
/** 処理結果メッセージを編集します */
public static String getMessage(String errCode) {
String msg = null;
// errCodeがnullの場合は処理成功。値が入っていた場合だけエラーメッセージを取得する。
if(errCode== null) {
msg = "処理は成功しました。";
} else {
msg = editMessage(errCode);
}
return msg;
}