Open4

Java言語(例外処理の基本)

zenn.lovegon17zenn.lovegon17

例外とは

  • Javaはコンパイル型言語のため、文法や変数の型に不具合があるとコンパイルによって検出
    • 実行したときに初めて検出される類のエラーがある
    • Javaプログラム実行中において発生するエラーのことを「例外」と呼ぶ
  • Javaでは、例外はプログラムを構成する要素の1つとしてクラスで表現する
    • Javaでは、エラーの種類は例外クラスによって表す

例外の様々なパターン

  • 例外の様々なパターン
    • プログラム不良に起因する例外:null参照やゼロ除算のようなシステム的な例外
      →プログラムを修正する
    • 環境に起因する例外:サーバやネットワークといったシステム基盤に起因する例外や、読み込もうとしたファイルが存在しないといった例外
      →そういったケースを想定した上で適切な措置が必要(プログラム不良ではない)
    • 残高不足により引き落としができない→業務上想定しえる(発生に備えて対応が必要)
    • 存在するはずのマスターデータがない→業務上想定しえない(必ずしも対応は必須ではない)

例外ハンドリングとは

  • 例外が発生すると処理は中断され、プログラムは停止する
  • 発生した例外は放置するのではなく、適切に対処することで、ログを出力したり、トランザクション(データベースや他のシステムに対する一連の操作をまとめたもの)をロールバック(トランザクションが途中で失敗したときに、それまでに行われたすべての変更を元に戻すこと)したり、業務的なリカバリーを行ったりする必要がある
  • 発生した例外に対して何らかの措置を施すことを、例外ハンドリングと呼ぶ

代表的な例外クラス

  • java.lang.ArithmeticException:ゼロ除算等、算術演算における不具合
  • java.lang.ArrayIndexOutOfBoundsExcepiton:配列の範囲を超えたインデックス指定
  • java.lang.NullPointerExcepiton:null値を持つ変数へのアクセス
  • java.lang.ClassCastExcepiton:互換性のない型へのキャスト
  • java.lang.CloneNotSupportedException:colneメソッド対象クラスがClonableインタフェースを未実装
  • java.lang.NumberFormatException:ラッパークラスのparseメソッド等で文字列を解析できない場合など
  • java.util.ConcurrentmodificationException:拡張for文によるリスト変更など並行処理によるデータ書き換え
  • java.text.ParseException:SimpleDateFormatクラスのparseメソッドで日時文字列を解析できない場合など
  • java.time.format.DateTimeParseException:LocalDateTimeクラスのparseメソッドで日時文字列を解析できない場合など
    →本スクラップではこれらの例外をJava SE例外と呼称

try-catch文による例外ハンドリング

  • Javaには、例外ハンドリングを行うための仕組みが言語仕様として備わっている
  • 例外ハンドリングを実現するためには、try-catch文を使用

try{
...例外が発生する可能性のある処理
}catch(例外クラス1 変数1){
...例外クラス1発生時の処理...
}catch(例外クラス2 変数2){
...例外クラス2発生時の処理
}

  • try-catch文は、1つのtryブロックと、複数のcatchブロックから構成
  • tryブロック内で条件分岐やループをしたり、return文を記述することも可能
  • tryブロックの中で例外が発生すると、処理はその時点で中断され、catchブロックへの進む
  • catchキーワードの後ろに()を記述し、そのブロックが対応する例外クラスと、例外オブジェクトを格納するための変数を指定
  • tryブロック内でいくつかの種類の例外が発生する可能性がある場合は、その種類に合わせてcatchブロックを複数記述する
  • 複数のcatchブロックが記述されている場合、上から順に発生した例外クラスとcatchブロックに指定した例外クラスのマッチングが行われ、マッチしたブロックに処理が進む
  • catchブロックによって特定の例外を捉えることを「例外を捕捉する」と言う
  • catchブロックには捕捉した例外のオブジェクトが変数として渡されるので、それを利用して適切なハンドリングを行う

例外クラスの種類と分類

  • 例外の中で最上位に位置するのがjava.lang.Throwableであり、あらゆる例外クラスの親

  • 例外クラスは以下のように分類される

    1. エラークラス
    2. 例外クラス
      2-1:非チェック例外クラス
      2-2:チェック例外クラス
  • エラークラス:メモリ不足など環境に起因する致命的な不具合を表す例外

    • 階層構造的には、java.lang.Errorの子クラス
    • 通常この例外が発生した場合、プログラムでの復旧は困難であり、JVMが強制終了されることも多いため、try-catch文による例外ハンドリングは行われない
  • 例外クラスは、非チェック例外(非検査例外)とチェック例外の2つに分類される

  • 非チェック例外

    • try-catch文による例外ハンドリングが任意の例外
    • 階層構造的には、java.lang.RuntimeExceptionの子クラス
    • NullPointerException例外はこの分類
  • チェック例外

    • try-catch文による例外ハンドリングがコンパイラによって強制される例外
    • 階層構造的には、java.lang.Exceptionの子クラス(ただしRuntimeExceptionの子クラスを除く)
    • ファイル入出力、ネットワーク入出力、DBアクセスなど、システム基盤や環境に起因する例外
    • チェック例外の目的は、発生の可能性がある問題に対してそのハンドリングを強制すること

例外クラスのAPI

  • java.lang.Throwable(例外クラスの最上位)に定義された主要なAPI
  • String getMessage():メッセージを取得
  • Throwable getCause():根本原因を取得
  • void printStackTrace():スタックトレースを(標準出力に)出力する
    • あらゆる例外クラスは、これらのAPIを保持してい
    • catchブロックで例外を捕捉したら、これらのAPIを呼び出すことによって様々な処理を行う
    • デバックを容易にするために、例外クラスが持つメッセージを取得したり、スタックトレースを出力したりする処理は、高い頻度で登場する
    • スタックトレース(例外が発生したときに、その例外がどこで、どのような経路で発生したかを示す情報。
zenn.lovegon17zenn.lovegon17

例外発生時の挙動

  • 非チェック例外(例外ハンドリングは任意)の具体例:例外ハンドリングを行わず

public class Main_1{
public static void main(String[] args){
int val1 = Integer.parseInt(args[0]);
int val2 = Integer.parseInt(args[1]);
int answer = val1 / val2;//①除算→例外発生の可能性
System.out.println(answer);

  • このクラスはコマンドライン引数を2つもち、それらの除算を行い、答えをコンソールに表示する
  • 第2引数に0を与えて実行すると、ゼロ除算によってArithmeticExcepiton例外が発生して処理が中断されるため、コンソールには答えは表示されない

例外ハンドリングの挙動

  • 非チェック例外(例外ハンドリングは任意)の具体例:例外ハンドリングを行う

public class Main_1{
public static void main(String[] args){
int val1 = Integer.parseInt(args[0]);
int val2 = Integer.parseInt(args[1]);
try{①
int answer = val1 /val2;②
System.out.println(answer);
}catch(ArithmeticExceptino ae){③
System.out.println("ゼロ除算発生"+ae.getMessage());④
}}}

  • ①tyrブロックの中に、例外が発生する可能性のある処理を記述
  • ②ゼロ除算が行われるとArithmeticExcption例外が発生
  • ③処理が中断された対応するcatchブロックに処理が進む
  • ④catchブロックでは、ArithmeticExceptionオブジェクトが変数aeとして渡されるため、変数aeからメッセージを取得し、それをコンソールに表示する
  • ここでは便宜上、ArithmeticExceptionをハンドリングする例を取り上げたが、ゼロ除算に対応するための正しい方法は、除算を行う前にif文でチェックする
zenn.lovegon17zenn.lovegon17

例外生成と送出のパターン別挙動

  • 例外クラスには、Java SE例外もあれば、ユーザー定義例外(開発者自身が作成する例外)もある

    • Java SE例外発生のパターンには、Javaランタイム(Javaプログラムを実行するための環境)内で発生するケースもあれば、開発者がJava SE例外を明示的に生成して発生させるケースもある
    • ユーザー定義例外(開発者自身が作成した例外)を生成して発生させるケースもある
  • パターンを整理すると...
    パターン1:Javaランタイム内の処理で、Java SE例外が発生する
    パターン2:開発者が明示的にJava SE例外を生成し、送出する
    パターン3:開発者がユーザー定義例外を生成し、送出する
    ※例外を明示的に発生させることを「送出する」という

  • パターン2や3では、new演算子によって例外オブジェクトを生成する

    • このとき例外クラスのコンストラクタが呼び出される
  • 例外クラスのコンストラクタは、最上位のjava.lang.Throwableと同じコンストラクタを定義し、superキーワードによって呼び出すケースが一般的

  • Throwableクラスにはメッセージ、根本原因という2つの属性があり、それらを初期化するためには以下のようなコンストラクタが必要

    • Throwable(String)...メッセージを初期化
    • Throwable(Throwable)...根本原因を初期化
    • Throwable(String,Throwable)...メッセージと根本原因を初期化
      →パターン2や3では、コンストラクタによって例外を生成したらそれを送出する
  • 例外を送出するためには、以下のようにする

throw 例外オブジェクト

  • throwキーワードに例外オブジェクトを指定して例外を送出する

例外生成と送出の具体例

  • 具体例:パターン2(開発者が明示的にJava SE例外を生成し送出するケース)

public class Main{
public static void main(String[] args){
int param = Integer.parseInt(args[0]);//第一引数をint型に変換
if(param < 0){//引数が0未満のときに例外を送出
throw new IllegalArgumentException("引数が0未満");
}
int answer =param*2;
System.out.println(answer);
}}

  • コマンドライン引数が0未満だった場合に、IllegalArgumentException(引数が不正という意味を表すJava SE例外)のオブジェクトを生成し、送出する
  • IllegalArgumentExceptionには、Exceptionクラスと同じくメッセージを引数に取るコンストラクタが定義されているため、それを呼び出してオブジェクトを生成
  • 生成したオブジェクトをthrowキーワードに指定して送出
zenn.lovegon17zenn.lovegon17

例外の伝播

  • 例外が発生すると、その例外は呼び出し元へ、さらにはその呼び出し元へと伝播する

public class Main{
public static void main(String[] args){
Foo foo = new Foo();
int length = foo.process(args[0]);
System.out.println(length);
}}

public class Foo{
public int process(String param){
Bar bar = new Bar();
int length = bar.process(param);
return length;
}}

public class Bar{
public int process(String param){
int length = param.length();
if(10<length){
throw new IllegalArgumentException("文字列長が過大");
}
return length;

  • 処理の流れ:Mainクラスのコマンドライン引数をMain→Foo→Barへと順に渡し、Barで文字列長を計算して返す

伝播中の例外ハンドリング

  • 伝播中の例外はtry-catch文によって捕捉可能

public class Foo{
public int process(String param){
try{
Bar bar = new Bar();
int length = bar.process(param):①
return length;②
}catch(IllegalArgumentException iae){③
System.out.println("計算不可"+iae.getMessage());
return 0;④
}}}

  • Bar呼び出し①で例外が発生してFooクラスに伝播されると、後続の処理②は行われず、catchブロック③へと処理が進む
  • catchブロックでは例外を捕捉したらその内容を表示し、リカバリー措置として0を返す④
  • try-catch文によって、Mainクラスには例外は伝播されない

チェック例外の送出とthrows句

  • チェック例外の目的:発生の可能性がある問題に対してその適切なハンドリングを強制すること
    • メソッド内の処理において、外部的な問題に起因して例外が発生する可能性がある場合は、チェック例外を送出し、呼び出し元に例外ハンドリングを強制する
  • チェック例外を送出する場合、メソッドは以下のように宣言する

戻り値型 メソッド名(...) throws 例外クラス{
...チェック例外が送出される処理...
}

  • 呼び出し元から見ると、「throws句があるメソッドを呼び出す場合は、送出される例外を自身でハンドリングしなければならない」ということを意味する
  • throws句には、例外クラスをカンマで区切って複数指定可能
    • 指定された複数の例外クラスを(catchブロックを列挙するなどして)ハンドリングが必要

チェック例外ハンドリングの具体例

  • 具体例:既出のBarクラスを修正し、引数から生成されたURLの文字列長を返す

public class Bar{
public int process(String param){
try{
URL url = new URL(param);①
return url.toString().length();
}catch(MalformedURLException mue){②
System.out.println("URL生成不可"+mue.getMessage());
return 0;③
}}}

  • BarクラスではURLクラス(java.net.URL)を使い、文字列からURLを生成①
  • URLクラスのコンストラクタはURL(String) throws MalformedURLExceptionという宣言
  • 文字列("foo://"など)がURLの要件を満たさないとMalformedURLException例外(チェック例外)が発生
    • この例外はチェック例外のため、URLを生成するときは例外ハンドリングが必要
  • catchブロック②でこの例外を捕捉し、リカバリー措置として0を返す③

チェック例外の伝播

  • 具体例:Barにて例外ハンドリングを行わずFooに任せることもできる

public class Bar{
public int process(String param) throws MalformedURLException{①
URL url = new URL(param);
return url.toString().length();
}}

  • throws句に例外クラスを指定すると、メソッド内で発生する例外を呼び出し元に伝播させることができる①
    • 呼び出し元であるFooでは、伝播された例外をハンドリングしないと、コンパイルエラー
    • Fooでも、同じようにthrows句を使えば、さらにその呼び出し元に例外ハンドリングが委譲される