フォーラム » つれづれ Java編 »
エラー処理(例外、戻り値、アサート)
Java言語でのプログラミングでエラーを扱うことについて
Java言語が提供するエラーを扱う機能としては、次の3つがある。
- 例外
- アサート
- メソッドの戻り値
例外¶
例外は、Error、検査例外、非検査例外(RuntimeException)の3つに分類される。
- Errorは原則キャッチせず発生したらプログラム終了とする。
- 検査例外は、検査例外をスローすると宣言されたメソッドを呼び出す側が例外を捌くコードを記述することをコンパイラが強制する。捌き方には、catch文で補足する方法と呼び出し側のメソッドがさらに呼び出し側に例外をスローする方法がある。
- 非検査例外は、非検査例外をスローするメソッドがあっても呼び出す側に例外を捌くことがまったく強制されない。
アサート¶
アサートは、Java SE 1.4から言語仕様に取り入れられ、プログラムの任意の箇所にassert文でtrueになるべき評価式を表明する。プログラム実行時にassert文で表明がtrue出ない場合、AssertionErrorが発せられる。
メソッドの戻り値¶
メソッドの戻り値は、メソッドがエラーを戻り値で示すもので、エラーを直接扱う機能ではなく、メソッドの機能の一つをエラーに割り当てるプログラミング手法によるもの。
C言語などエラーを直接扱う機能のない言語では戻り値でエラーを示すことが多い。
戻り値のあるメソッドを呼び出す側が戻り値を無視できる(呼び出し側が変数に戻り値を格納しない)ので、エラーを捌くことがまったく強制されない。
メソッドがクエリー系(処理結果のデータを返す)の場合、エラーの値と処理結果の値と複数の値を呼び出し側に返す必要がある。
Javaのエラー処理をめぐる様々な考え¶
以降のスレッドで書き連ねていきます。
返答 (2)
エラーに関する技術資料 - 高橋 徹 さんが3年以上前に追加
Brian Goetz氏がOpenJDKのメーリングリスト で紹介していた記事。
著者 Joe Duffy氏は、現在Plumi社の創設者でCEO、前職はMicrosoft社でMidori OSプロジェクトや.NET、開発ツール関係に携わる。
書籍「Professional .NET Framework 2.0(2006)」、「Concurrent Programming on Windows(2008)」の著者。
記事の要点
導入¶
- 大半の言語は、バグと回復可能エラーを一緒に扱い同じ仕組みで対応しているが、コードの信頼性を損ないやすい
→ 2つのエラーモデルを提供すべき- 1つはプログラミングのバグを扱うフェイルファースト(中断:abandonment)
- もう1つは回復可能なエラーに対する静的な検査例外
企図と学習¶
- アーキテクチャの原則、要件、既存からの学習
原則・要件¶
- 使いやすい(エラーに直面したときに正しく事をなせる)
- 信頼できる
- 性能がよい
- 並行できる
- 診断可能
- 構成できる(プログラミングに組み込みやすい)
学習¶
*既存のエラーモデルは上述要件をすべて満たすものがない。例)エラーコードによるモデルは、信頼できるが使いやすさに欠ける(間違ったことが簡単にできる)
- スクリプト言語の用途では信頼性より使いやすさに重きを置くので、OSに近いシステムプログラミングとは結論が異なる
- JavaやC#は両方の用途があるのでエラーモデルの提供に苦慮があるが、我々の(システムプログラミング)ニーズを満たせていない
- 最近の言語であるGo、Rust、Swiftではエラーモデルの改良がある
エラーコード¶
戻り値でエラーコードを示す方法でもっとも単純なエラーモデル。関数が成功か失敗を値で返すので、エラー機能の提供を必要としない。
例えば、関数の戻り値が0なら成功、0以外なら失敗を意味する。エラーの追加情報を提供する手段(errno変数、GetLastError関数等)もある。
Go言語はエラーコードをエラーモデルに採用している。
関数型言語は、モナドやOption、Maybe、Eitherなどに被せた戻り値をエラーモデルに採用している。
デメリットは
- 性能の低下
本来の戻り値とエラーの戻り値と2つの値を返却するためにレジスタとスタックを消費する - プログラミングモデルの使いやすいさを損なう
呼び出し側に処理の分岐が必要となる - エラーの検査を忘れる深刻な問題
関数呼び出しで戻り値を無視するのは簡単
例外¶
- 非チェック例外
型システム、シグニチャには含まずに例外を生成しスローし補足する。
すべての関数呼び出しで例外スローの可能性があり、それに対処するのは大変。
C++では「例外安全性」の手法で対処。
C++のリアルタイムおよびミッションクリティカルシステムのプログラムでは例外を使わない。
- 検査例外
Javaプログラマーの大半からぼこぼこにされているが、非チェック例外の混乱に比べると不当な扱いを受けているように見える。
Javaではメソッドの宣言に例外を記述するため、メソッドが何をスローするか分かる。
呼び出し側は、スローされた例外を1)そのまま上位に伝播する、2)キャッチして処理する、3)例外を翻訳する、のいずれかの処理をする。
- 例外全般
例外のスローに大きなコストがかかる(スタックトレースの収集)
例外はエラー処理の粒度が粗くなりがち(エラー処理は特定の関数呼び出しにフォーカスすべき)
例外のスロー時の制御フローが見えない。Javaではシグニチャに例外型が含まれるので幾分よいが、例外がどこからきたのか正確に確認することが難しい。
バグは回復可能エラーでない¶
- 回復可能エラーは通常データの検証結果で、プログラムは回復することが期待される。よって失敗した場合の対処を考える必要がある。利用者に通知する、再試行する、操作を中断する、等。
- バグは予期しないエラーで、入力の検査が不正確、ロジック誤り等。すぐに検出されないことがよくあり、問題が顕在化するまで時間を要し、プログラムに重大な損傷が発生しているかもしれない。
信頼性、耐障害性、分離¶
信頼できるシステムの構築¶
個々の部分に障害が発生しても全体が機能し続けるようにシステムを設計し、障害のある部分を正常に回復するようシステムに知らせること。分散システムではお馴染みの方法。
放棄(Abandonment)¶
バグ:放棄、アサーション、契約¶
T.B.D.
JJUG CCC 2021 Spring A2「今どき?のJavaにおける例外処理についての考察」 - 高橋 徹 さんが3年以上前に追加
JJUG CCC 2021 Spring 「今どき?のJavaにおける例外処理についての考察」
セッションのポイント
- 検査例外は、throws句がコード上のノイズ、try-catch構文は可読性低下
例外握り潰し誘因 - 上位レイヤーに下位レイヤーの関心語(例外型)が漏洩
例外翻訳は、例外の状況(例、DBで例外)が分からなくなり対処(例、ロールバック)ができなくなるからと一刀両断 - Spring frameworkがデファクトスタンダードで、こいつはインフラ層から上がる例外がすべてRuntime Exceptionである。Runtime ExceptionをAOP・フィルタ・インタセプターで対処(メソッド名と例外型でエラー処理を差し込む?)し、ビジネスロジックでは例外処理を記述しない
- アプリケーション・ドメイン層で扱うべきエラーは例外ではなく戻り値として返却すべき
- DBトランザクションは原則ビジネスルール違反を事前検査しロールバックを起こさせないようにすべき
- 戻り値はエラー状態用のクラスを定義して返す
Go言語のように多値を返却できるとよいが、Javaではできないのでサブタイプで正常・エラーを定義
感想
- Spring framework上のアプリケーションにおける例外処理というコンテキストでの議論なので、Javaプログラミング全体には敷衍できない
- 戻り値で都度エラーチェックは、try-catch構文と同等以上に可読性が悪いと思う
- 戻り値も握り潰しできるし、コード上握り潰しが不明瞭(戻り値がvoidなのか、あるのに無視したのか字面上は区別できず)
戻り値の握り潰しの反省から検査例外が登場したので、その点では逆行 - Javaは戻り値を1つしか返却できないので、エラー状態を含む戻り値専用型を多数(ジェネリクス使えば1つの標準戻り値型?)作成
- 例外翻訳は例外連鎖を使って発生源の例外インスタンスを保持できる