i-Vinci TechBlog
株式会社i-Vinciの技術ブログ

例外処理と上手に付き合おう

皆さんこんにちは。i-Vinci 下位です。

最近とても暑いですね。
東京は気温が 35 度を超える状態になってしまってとても辛い。
もう少し手心を加えた気温になって頂きたいところです。

さて、本日のテーマは例外処理です。

例外処理とは

例外処理とは、「想定外の挙動に対する制御処理」です。
環境面の問題であったり、予期しないユーザー操作等で発生します。

前者はシステム例外、後者は業務例外と区分けされたりもします。
主な違いは下記の通りです。

呼称 特性 代表例
システム例外 システム都合で発生する例外。システム管理者の対応が必要。 DB or サーバーが停止している等
業務例外 業務ルールから逸脱する様なデータ入力起因で発生する例外。入力値の変更等で解消可能。 郵便番号に数字以外を入力する等

区分け自体は決めの問題です。
プロジェクト特性によっては他の区分の例外も取り扱っている可能性があります。

大事なのは手当をしなければプログラムが停止するということです。
何をしていようが問答無用で止まります。

お手元のシステムで、特定の文字を入力すると「想定外の問題が発生しました〜」的なメッセージが出たらそれは例外が発生したということです。
シングルクォーテーションとか入力すると出てくるケースが稀にありますので気をつけましょう。

例外の復帰処理

前項でも記載しましたが、例外が発生した時点で処理が止まります。
その為、止まっては困る処理には例外の復帰処理を記載する必要があります。(java でいえば try-catch 構文)

復帰が難しいシステム例外であれば、潔く終わらせましょう。

try {
    // 適当な処理
} catch (Exception e) {
    e.printStackTrace();
    // ダイアログの出力
    // システムエラーが発生しました。システム管理者に連絡してください
    System.Exit(1);
}

入力値を修正すれば継続できそうなものなら値を補正して継続させます。(復帰処理)

String input = "abc";
int data;

try {
    data = Integer.parseInt(input);
} catch (NumberFormatException e) {
    // 不正値の場合は初期値に補正...etc
    data = 0;
}

下層の処理で発生した例外は catch されない限り、上層の処理に例外が転送されます。

void coreFunc() {
    try {
        // この処理で例外が発生する
        subFunc("abc");
    } catch (NumberFormatException e) {
        // subFuncで送出された例外はここで捕捉されます
    }
}

// 引数を数値に変換するサブ処理です
// 数値に変換できない文字が入力されると例外が発生します
int subFunc(String input) {
    return Integer.parseInt(input);
}

余談ですが、例外の中継プレーは無意味なのでやめましょう。
catch を省略すれば上位に例外が転送されるので同じ意味合いになります。

try {
    return Integer.parseInt("test");
} catch (Exception e) {
    // リスローするだけならtry-catchを省略しましょう。結果は同じです。
    throw e;
}

上記の処理はこの処理と等価になります。

    return Integer.parseInt("test");

捕捉した例外情報をキャストする場合は、根本原因の例外インスタンスを引数に渡しましょう。
引数設定を忘れると、例外発生の根本原因特定が難しくなります。
そして保守担当者に刺されます

public class Program {
    public static void main(String args[]) throws Exception {
        try {
            var result = Integer.parseInt("abc");
        } catch (NumberFormatException e) {
            // 例外インスタンスが渡されていないので根本原因は引き継がれません
            throw new Exception(); // 例外送出ポイント
        }
    }
}

上記処理から生まれる StackTrace です。うーんわからん。

Exception in thread "main" java.lang.Exception
    at Program.main(Program.java:6)

引数設定すればこの通り。引数大事です。

Exception in thread "main" java.lang.Exception: java.lang.NumberFormatException: For input string: "abc"
    at Program.main(Program.java:6)
Caused by: 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:668)
    at java.base/java.lang.Integer.parseInt(Integer.java:784)
    at Program.main(Program.java:4)

色々脱線しましたが、基本としてはこんな感じです。

例外を生成する

前述の通り、例外が発生するとプログラムは止まります。
言い換えると、自分で例外を生成できれば好きなタイミングでプログラムを止めることができます。

郵便番号を例に考えてみましょう。
入力規則は以下の通りです。

  1. 構成文字列は数値 7 桁である
  2. 数値(0-9)以外の文字入力は許容しない

この郵便番号に数字以外の文字が入力されたら困らないでしょうか。
異常な入力を検知した際にプログラムを止めてしまいたい。

こんな時は自分で例外を生成してしまいましょう。

void validatePostCode(String input) {
    if (Objects.isNull(input) || !input.matches("^[0-9]{7}$")) {
        // 不適切な値の場合は自分で例外を投げてしまいます。
        throw new IllegalArgumentException("郵便番号として不正な値です");
    }
}

これで郵便番号として不適切な値が入力された場合は例外が送出されるようになりました。
例外を送出することで得られるメリットですが、異常な処理であるから止めてしまいたいということをプログラム上で明示できることにあります。

catch しない限りプログラムが停止するので、異常な結果を無視して処理を継続してしまったという事故を削減できます。
try-catch で括っていれば復帰可能ですが、異常結果を握りつぶすような catch は最早確信犯なので無視します

カスタム例外を作る

前項では自分自身による例外送出について記載しました。
言語から提供される例外クラスは数多く用意されていますが、汎用的すぎて詳細なエラー表現に難儀することがあります。
そんな時はカスタム例外クラスを作成しましょう。

Java 言語仕様の話になりますが、Java で扱う例外には 2 つの種類があります。

種類 スーパークラス 備考
検査例外 java.lang.Exception 例外処理を実装は必須。対処しなければコンパイルエラーとなる
非検査例外 java.lang.RuntimeException 例外処理の実装は任意。多くは実行時例外を示す

表に示した何れかのスーパークラスを継承することで、自作の例外クラスが作成可能です。
自作したい例外の特性を見極めて、適切なスーパークラスを継承するようにしましょう。

今回は一例として、パラメータの書式不正を示す例外クラスを作成し、郵便番号チェック処理に適用してみます。
想定対象は外部入力のパラメータですので、実行時例外を示す非検査例外で実装していきます。

// IllegalArgumentExceptionはRuntimeExceptionのサブクラスです。つまり非検査例外になります。
class ParameterFormatException extends IllegalArgumentException {
    // 保持したい情報を増やす場合は、フィールドを宣言することで対応可能です。

    // コンストラクタ
    public ParameterFormatException(String msg) {
        super(msg);
    }
}

// 先程の業務例外処理に適用します
void validatePostCode() {
    if (Objects.isNull(input) || !input.matches("^[0-9]{7}$")) {
        throw new ParameterFormatException("郵便番号として不正な値です");
    }
}

上記処理の StackTrace をみてましょう。

Exception in thread "main" ParameterFormatException: 郵便番号として不正な値です。
    at Program.validatePostCode(Program.java:10)
    at Program.main(Program.java:5)

例外インスタンスが切り替わったことにより、命名から原因の推測も容易になったと感じませんか。
他のクラスや変数等と同様、例外クラスも適切な命名をすることで、システムの保守性を向上させることが可能です。

(例えば、IndexOutOfBoundsException のログ出力があれば、入力データが用意した配列領域に収まらなかったんだなという推測が付きます。)

参考までに、Exception を送出した場合の StackTrace です。
StackTrace ログが生きていれば調査自体には問題ありませんが、前述のログとは印象が違って見えないでしょうか。

Exception in thread "main" java.lang.Exception: 郵便番号として不正な値です。
    at Program.validatePostCode(Program.java:10)
    at Program.main(Program.java:5)

まとめ

いかがでしたか。今回は例外処理について注目してみました。

例外は怖くありません。適切に使えば見通しの良いプログラミング構造にできる強力な機能です。

皆さんもカッコイイ独自例外、ガンガン作りましょう!