他の人はなぜ早く投げるのかをかなりよく要約しました。代わりに後半部分をキャッチする理由に集中してみましょう。これについては、私の好みについて満足のいく説明がありませんでした。
なぜ例外なのか?
そもそも例外が存在する理由についてはかなり混乱しているようです。ここで大きな秘密を共有しましょう:例外の理由と例外処理は... ABSTRACTIONです。
このようなコードを見ましたか:
static int divide(int dividend, int divisor) throws DivideByZeroException {
if (divisor == 0)
throw new DivideByZeroException(); // that's a checked exception indeed
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
try {
int res = divide(a, b);
System.out.println(res);
} catch (DivideByZeroException e) {
// checked exception... I'm forced to handle it!
System.out.println("Nah, can't divide by zero. Try again.");
}
}
これは、例外の使用方法ではありません。上記のようなコードは実際に存在しますが、より異常なものであり、実際には例外です(しゃれ)。たとえば、純粋な数学であっても、除算の定義は条件付きです。入力ドメインを制限するためにゼロの例外的なケースを処理する必要があるのは、常に「呼び出し元コード」です。それは醜いです。発信者にとって常に苦痛です。それでも、このような状況では、check-then-doパターンが自然な方法です。
static int divide(int dividend, int divisor) {
// throws unchecked ArithmeticException for 0 divisor
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
if (b != 0) {
int res = divide(a, b);
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
または、次のようにOOPスタイルで完全なコマンドを実行できます。
static class Division {
final int dividend;
final int divisor;
private Division(int dividend, int divisor) {
this.dividend = dividend;
this.divisor = divisor;
}
public boolean check() {
return divisor != 0;
}
public int eval() {
return dividend / divisor;
}
public static Division with(int dividend, int divisor) {
return new Division(dividend, divisor);
}
}
static void doDivide() {
int a = readInt();
int b = readInt();
Division d = Division.with(a, b);
if (d.check()) {
int res = d.eval();
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
ご覧のとおり、呼び出し元のコードには事前チェックの負担がありますが、その後の例外処理は行いません。場合はArithmeticException
、これまでの呼び出しから来ているdivide
か、eval
そして、それはあなたがあなたが忘れてしまったので、例外処理を行うと、あなたのコードを修正しなければならない人check()
。同様の理由で、aをキャッチすることNullPointerException
はほとんど常に間違ったことです。
現在、メソッド/関数のシグネチャの例外的なケースを見たい、つまり出力ドメインを明示的に拡張したいという人々がいます。彼らはチェック例外を好むものです。もちろん、出力ドメインを変更すると、直接の呼び出し元コードを強制的に適合させる必要があります。これは、チェック例外を使用して実現できます。しかし、そのための例外は必要ありません!そのため、Nullable<T>
ジェネリッククラス、ケースクラス、代数データ型、およびユニオン型があります。一部のオブジェクト指向の人々は、次のような単純なエラーの場合に戻る ことを好むかもしれませんnull
:
static Integer divide(int dividend, int divisor) {
if (divisor == 0) return null;
return dividend / divisor;
}
static void doDivide() {
int a = readInt();
int b = readInt();
Integer res = divide(a, b);
if (res != null) {
System.out.println(res);
} else {
System.out.println("Nah, can't divide by zero. Try again.");
}
}
技術的には、上記のような目的で例外を使用できますが、ここにポイントがあります。そのような使用法には例外は存在しません。例外は抽象概念です。例外は、間接指定に関するものです。例外により、直接的なクライアント契約を破ることなく「結果」ドメインを拡張し、エラー処理を「他の場所」に延期することができます。コードが、同じコードの直接の呼び出し元で処理される例外をスローし、その間に抽象レイヤーが含まれていない場合、それは間違っています
遅延をキャッチする方法は?
だからここにいる。上記のシナリオで例外を使用することは、例外をどのように使用するかではないことを示すために、私の方法を議論しました。ただし、例外処理によって提供される抽象化と間接化が不可欠である、真のユースケースが存在します。そのような使用法を理解することは、キャッチレイトの推奨事項の理解にも役立ちます。
その使用例は次のとおりです。リソース抽象化に対するプログラミング ...
ええ、ビジネスロジックは、具体的な実装ではなく、抽象化に対してプログラムする必要があります。トップレベルのIOC「配線」コードは、リソース抽象化の具体的な実装をインスタンス化し、それらをビジネスロジックに渡します。ここに新しいものはありません。しかし、これらのリソース抽象化の具体的な実装は、潜在的に独自の実装固有の例外をスローする可能性がありますか?
実装固有の例外を処理できるのは誰ですか?その場合、ビジネスロジックでリソース固有の例外を処理することは可能ですか?いいえ、そうではありません。ビジネスロジックは抽象化に対してプログラムされており、これらの実装固有の例外の詳細に関する知識は除外されます。
「Aha!」と言うかもしれませんが、「だから例外をサブクラス化して例外階層を作成できるのです」(Mr. Springをチェックしてください!)。誤解です。まず、OOPに関するすべての合理的な本は、具体的な継承は悪いと述べていますが、JVMのこのコアコンポーネントである例外処理は、具体的な継承と密接に結びついています。皮肉なことに、Joshua Blochは、有効なJavaの本を書く前に、動作するJVMでの経験を得ることができませんでしたか?それは、次世代のための「学んだ教訓」の本です。第二に、そしてより重要なこととして、高レベルの例外をキャッチした場合、どのようにそれを処理しますか?PatientNeedsImmediateAttentionException
:私たちは彼女にピルを与えるか、彼女の足を切断する必要があります!?考えられるすべてのサブクラスに対するswitchステートメントはどうですか?ポリモーフィズムがあり、抽象化があります。あなたはポイントを得ました。
それでは、リソース固有の例外を誰が処理できますか?結石を知っている人でなければなりません!リソースをインスタンス化した人!もちろん「配線」コード!これをチェックしてください:
抽象化に対してコーディングされたビジネスロジック...コンクリートリソースエラー処理なし!
static interface InputResource {
String fetchData();
}
static interface OutputResource {
void writeData(String data);
}
static void doMyBusiness(InputResource in, OutputResource out, int times) {
for (int i = 0; i < times; i++) {
System.out.println("fetching data");
String data = in.fetchData();
System.out.println("outputting data");
out.writeData(data);
}
}
一方、どこか他の具体的な実装...
static class ConstantInputResource implements InputResource {
@Override
public String fetchData() {
return "Hello World!";
}
}
static class FailingInputResourceException extends RuntimeException {
public FailingInputResourceException(String message) {
super(message);
}
}
static class FailingInputResource implements InputResource {
@Override
public String fetchData() {
throw new FailingInputResourceException("I am a complete failure!");
}
}
static class StandardOutputResource implements OutputResource {
@Override
public void writeData(String data) {
System.out.println("DATA: " + data);
}
}
そして最後に、配線コード...具体的なリソース例外を処理するのは誰ですか?それらについて知っている人!
static void start() {
InputResource in1 = new FailingInputResource();
InputResource in2 = new ConstantInputResource();
OutputResource out = new StandardOutputResource();
try {
ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
}
catch (FailingInputResourceException e)
{
System.out.println(e.getMessage());
System.out.println("retrying...");
ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
}
}
今私と一緒に耐えます。上記のコードは単純です。IOCコンテナー管理リソースの複数のスコープを持つエンタープライズアプリケーション/ Webコンテナーがあり、セッションまたはリクエストスコープリソースの自動再試行と再初期化などが必要であると言うかもしれません。リソースを作成するため、正確な実装を認識しません。高レベルのスコープのみが、それらの低レベルのリソースがスローできる例外を本当に知っているでしょう。ちょっと待って!
残念ながら、例外はコールスタックを介した間接化のみを許可し、異なるカーディナリティを持つ異なるスコープは通常、複数の異なるスレッドで実行されます。例外を除き、それを介して通信する方法はありません。ここにはもっと強力なものが必要です。回答:非同期メッセージパッシング。下位レベルのスコープのルートですべての例外をキャッチします。何も無視しないでください。何もすり抜けないでください。これにより、現在のスコープの呼び出しスタックで作成されたすべてのリソースが閉じられ、破棄されます。次に、例外が既知のレベルに達するまで、例外処理ルーチンでメッセージキュー/チャネルを使用して、エラーメッセージを上位のスコープに伝達します。それはそれを処理する方法を知っている人です。
SUMMA SUMMARUM
だから、私の解釈によれば、キャッチレイトとは、あなたがこれ以上アブストラクトを壊さない最も便利な場所で例外をキャッチすることを意味します。早くキャッチしないでください!リソース抽象化のインスタンスをスローする具体的な例外を作成するレイヤー、抽象化の結石を知っているレイヤーで例外をキャッチします。「配線」層。
HTH。ハッピーコーディング!