到達不能なコードで新しいRuntimeExceptionをスローするのは悪いスタイルですか?


31

少し前に、より熟練した開発者によって書かれたアプリケーションを保守することになりました。私はこのコードに出会いました:

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
        try {
            return translate(mailManagementService.retrieveUserMailConfiguration(id));
        } catch (Exception e) {
            rethrow(e);
        }
        throw new RuntimeException("cannot reach here");
    }

投げることRuntimeException("cannot reach here")が正当化されるかどうか興味があります。私はおそらく、このコードがより経験豊かな同僚から来ていることを知っている明らかな何かを見逃しています。
編集:ここにいくつかの答えが言及したボディを再スローします。この質問では重要ではないと判断しました。

private void rethrow(Exception e) throws MailException {
        if (e instanceof InvalidDataException) {
            InvalidDataException ex = (InvalidDataException) e;
            rethrow(ex);
        }
        if (e instanceof EntityAlreadyExistsException) {
            EntityAlreadyExistsException ex = (EntityAlreadyExistsException) e;
            rethrow(ex);
        }
        if (e instanceof EntityNotFoundException) {
            EntityNotFoundException ex = (EntityNotFoundException) e;
            rethrow(ex);
        }
        if (e instanceof NoPermissionException) {
            NoPermissionException ex = (NoPermissionException) e;
            rethrow(ex);
        }
        if (e instanceof ServiceUnavailableException) {
            ServiceUnavailableException ex = (ServiceUnavailableException) e;
            rethrow(ex);
        }
        LOG.error("internal error, original exception", e);
        throw new MailUnexpectedException();
    }


private void rethrow(ServiceUnavailableException e) throws
            MailServiceUnavailableException {
        throw new MailServiceUnavailableException();
    }

private void rethrow(NoPermissionException e) throws PersonNotAuthorizedException {
    throw new PersonNotAuthorizedException();
}

private void rethrow(InvalidDataException e) throws
        MailInvalidIdException, MailLoginNotAvailableException,
        MailInvalidLoginException, MailInvalidPasswordException,
        MailInvalidEmailException {
    switch (e.getDetail()) {
        case ID_INVALID:
            throw new MailInvalidIdException();
        case LOGIN_INVALID:
            throw new MailInvalidLoginException();
        case LOGIN_NOT_ALLOWED:
            throw new MailLoginNotAvailableException();
        case PASSWORD_INVALID:
            throw new MailInvalidPasswordException();
        case EMAIL_INVALID:
            throw new MailInvalidEmailException();
    }
}

private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
    switch (e.getDetail()) {
        case LOGIN_ALREADY_TAKEN:
            throw new MailLoginNotAvailableException();
        case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
            throw new MailEmailAddressAlreadyForwardedToException();
    }
}

private void rethrow(EntityNotFoundException e) throws
        MailAccountNotCreatedException,
        MailAliasNotCreatedException {
    switch (e.getDetail()) {
        case ACCOUNT_NOT_FOUND:
            throw new MailAccountNotCreatedException();
        case ALIAS_NOT_FOUND:
            throw new MailAliasNotCreatedException();
    }
}

12
rethrow実際throwに例外に失敗した場合、技術的に到達可能です。(実装が変更された場合、いつか発生する可能性があります)
-njzk2

34
余談ですが、AssertionErrorはRuntimeExceptionよりも意味的に優れた選択かもしれません。
JvR

1
最後のスローは冗長です。findbugsまたはその他の静的分析ツールを有効にすると、これらのタイプの行に削除のフラグが付けられます。
ボン亜美

7
残りのコードが表示されたので、「スキルの高い開発者」を「上級開発者」に変更する必要があると考えています。コードレビューはその理由を喜んで説明します。
JvR

3
例外が恐ろしい理由の仮説例を作成しようとしました。しかし、私がこれまでにやったことは何もありません。
ポールドレーパー

回答:


36

最初に、質問を更新して、何を見せてくれてありがとう rethrow。したがって、実際には、プロパティを持つ例外をより詳細な例外クラスに変換します。これについては後で詳しく説明します。

私はもともと主な質問に実際に答えなかったので、ここに行きます。はい、到達不能コードでランタイム例外をスローするのは一般に悪いスタイルです。アサーションを使用するか、問題を回避することをお勧めします。既に指摘したように、ここのコンパイラは、コードがtry/catchブロック。あなたはそれを活用することでコードをリファクタリングすることができます...

エラーは値です

(当然、goでよく知られています

編集する前に使用した、より単純な例を使用してみましょう。何かを記録し、Konradの答えのようにラッパー例外を作成していると想像してください。。それを呼び出しましょうlogAndWrap

の副作用として例外をスローする代わりに、副作用としてlogAndWrap動作させ、例外(少なくとも、入力で指定されたもの)を返すようにすることができます。ジェネリックを使用する必要はなく、基本的な機能のみを使用します。

private Exception logAndWrap(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    return new CustomWrapperException(exception);
}

次に、throw明示的に、あなたのコンパイラは幸せです:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     throw logAndWrap(e);
}

忘れたらどうしますthrowか?

Joe23のコメントで説明されているように、例外が常にスローされるようにするための防御的なプログラミング方法は、Guava.Throwablesによって行われるようにthrow new CustomWrapperException(exception)、最後に明示的に行うlogAndWrapことです。そうすれば、例外がスローされることがわかり、タイプアナライザーが満足します。ただし、カスタム例外は未チェックの例外にする必要がありますが、これは常に可能とは限りません。また、開発者が書くことができないリスクを評価しますthrow非常に低く評価します。開発者はそれ忘れなければならず、周囲のメソッドは何も返すべきではありません。これは型システムと戦うための興味深い方法ですが、動作します。

再スロー

実際のrethrowものも関数として書くことができますが、現在の実装には問題があります:

  • のような多くの役に立たないキャストがあります 実際にキャストが必要です(コメントを参照):

    if (e instanceof ServiceUnavailableException) {
        ServiceUnavailableException ex = (ServiceUnavailableException) e;
        rethrow(ex);
    }
  • 新しい例外をスロー/返す場合、古い例外は破棄されます。次のコードでは、a MailLoginNotAvailableExceptionはどのログインが利用できないかを知ることができません。これは不便です。さらに、スタックトレースは不完全になります。

    private void rethrow(EntityAlreadyExistsException e)
        throws MailLoginNotAvailableException, MailEmailAddressAlreadyForwardedToException {
        switch (e.getDetail()) {
            case LOGIN_ALREADY_TAKEN:
                throw new MailLoginNotAvailableException();
            case EMAIL_ADDRESS_ALREADY_FORWARDED_TO:
                throw new MailEmailAddressAlreadyForwardedToException();
        }
    }
  • 元のコードがそもそもこれらの特殊な例外をスローしないのはなぜですか?私はそれを疑うrethrow(郵送)サブシステムとbusineessロジック(多分意図は、カスタム例外によってそれらを交換することによってスローされた例外のような非表示の実装の詳細にある)間の互換性層として使用されています。私はあることに同意した場合でもキャッチフリーのコードを持っているピート・ベッカーの答えで提案されているように、それは、良いだろう、私はあなたが削除する機会があるでしょうとは思わないcatchし、rethrow大きなリファクタリングすることなく、ここにコードを。


1
キャストは無駄ではありません。引数タイプに基づく動的なディスパッチはないため、これらは必須です。引数の型は、正しいメソッドを呼び出すためにコンパイル時に認識されている必要があります。
Joe23

あなたにはlogAndWrapこの方法は、代わりにそれを返すの例外をスローしますが、戻り値の型を保つことができます(Runtime)Exception。このように、catchブロックは記述したままにできます(到達不能コードをスローすることなく)。しかし、それはあなたが忘れることができないという追加の利点がありますthrow。例外を返し、スローを忘れると、例外が静かに飲み込まれます。戻り値の型RuntimeExceptionを持っているときにスローすると、コンパイラーは幸せになりますが、これらのエラーも回避できます。それがグアバのThrowables.propagate実装方法です。
ジョー23

@ Joe23静的ディスパッチに関する説明をありがとう。関数内での例外のスローについて:説明している可能性のあるエラーは、呼び出されたキャッチブロックでありlogAndWrap、返された例外では何もしないという意味ですか?それは面白い。値を返さなければならない関数の内部では、コンパイラーは値が返されないパスがあることに気づきますが、これは何も返さないメソッドに役立ちます。再度、感謝します。
コアダンプ

1
それがまさに私が言及していることです。そして、ポイントを強調するために、それは私が思いついて信用したものではなく、グアバの情報源から集めたものです。
Joe23

@ Joe23私は、ライブラリを明示的に参照を追加
コアダンプ

52

この rethrow(e);関数は、通常の状況下では関数が戻り、例外的な状況下では例外がスローされるという原則に違反しています。この関数は、通常の状況下で例外をスローすることにより、この原則に違反します。それがすべての混乱の原因です。

コンパイラーは、この関数が通常の状況下で戻ると想定しているため、コンパイラーが知る限り、実行はretrieveUserMailConfiguration関数の最後に到達する可能性があり、その時点でreturnステートメントがないことはエラーになります。そこにRuntimeException投げられるのは、コンパイラのこの懸念を軽減することになっていますが、それはやや不格好なやり方です。function must return a valueエラーを防ぐ別の方法は、return null; //to keep the compiler happyステートメントを追加することですが、それは私の意見では同様に不格好です。

だから、個人的に、私はこれを置き換えます:

rethrow(e);

これとともに:

report(e); 
throw e;

または、より良い、(コアダンプが示唆したように)、これで:

throw reportAndTransform(e);

したがって、制御の流れがコンパイラーに明らかになるため、コンパイラーthrow new RuntimeException("cannot reach here");が到達不能コードとしてフラグを立てるため、ファイナルは冗長になるだけでなく、実際にはコンパイルできなくなります。

これは、このelegantい状況から抜け出す最もエレガントで実際に最も簡単な方法です。


1
-1関数を2つのステートメントに分割することを提案すると、不正なコピー/貼り付けによるプログラミングエラーが発生する場合があります。私が自分の答えを投稿してから数分後にあなたが質問を編集して提案しthrow reportAndTransform(e)たという事実も少し心配です。多分あなたはそれとは無関係にあなたの質問を修正し、そうであれば事前に謝罪しますが、そうでなければ、少し信用を与えることは通常人々がすることです。
コアダンプ

4
@coredump実際にあなたの提案を読みましたが、この提案をあなたに帰する別の答えも読みました。私は実際に過去にそのような構造を正確に採用していたことを思い出したので、それを私の答えに含めることにしました。ここで元のアイデアについて話しているわけではないので、帰属を含めるほど重要ではないと思いましたが、あなたがそれについて問題を抱えている場合、私はあなたを悪化させたくないので、帰属は今そこにあります、リンクを完了します。乾杯。
マイクナキス

私がこのコンストラクトの特許を持っているわけではありません。これは非常に一般的です。しかし、あなたが答えを書くと想像してみて、まもなく、別のものによって「同化」されます。したがって、帰属は大歓迎です。ありがとうございました。
コアダンプ

3
返されない関数自体には何も問題はありません。たとえば、リアルタイムシステムでは常に発生します。究極の問題は、言語の正確さの概念を言語が分析および実施するが、関数が返されないことを何らかの方法で注釈付けするメカニズムを言語が提供しないというJavaの欠陥です。ただし、これを回避するデザインパターンがあります throw reportAndTransform(e)。多くのデザインパターンは、言語機能が欠落しているパッチです。これはそれらの1つです。
デビッドハンメン

2
一方、現代の多くの言語は十分に複雑です。すべての設計パターンをコア言語機能に変えると、さらに大きくなります。これは、コア言語に属さないデザインパターンの良い例です。書かれているように、コンパイラだけでなく、コードを解析する必要のある他の多くのツールでもよく理解されています。
–MSalters

7

throw何がいることを理解していないためにスマート十分であるJavaのデータフロー解析-おそらく、そうでない場合に発生する「メソッドが値を返さなければなりません」というエラーが周りを取得するために追加されたreturn後に必要でthrowはなく、カスタム後rethrow()方法、および何もありません@NoReturn注釈これを修正するために使用できます。

それにもかかわらず、到達不能なコードで新しい例外を作成することは不要なようです。return nullそれが実際に決して起こらないことを知って、私は単に書くでしょう。


あなたが正しいです。throw new...行をコメントアウトすると何が起こるかを確認しましたが、それはreturnステートメントがないことを訴えます。それは悪いプログラミングですか?到達不能なreturnステートメントを置き換えることに関する規約はありますか?さらなる入力を促すために、この質問にはしばらく答えないままにします。それにもかかわらずあなたの答えをありがとう。
ソクポマランチョビ

3
@SokPomaranczowyええ、それは悪いプログラミングです。このrethrow()メソッドはcatch、例外を再スローしているという事実を隠します。私はそのようなことをするのを避けます。さらに、もちろん、rethrow()実際には例外の再スローに失敗する可能性があり、その場合は別のことが起こります。
ロスパターソン

26
例外をに置き換えないでくださいreturn null。例外により、将来誰かがコードを壊して最後の行が何らかの方法で到達可能になった場合、例外がスローされるとすぐにクリアされます。代わりreturn nullにを使用すると、nullアプリケーション内に予期しない値が浮かんできます。アプリケーションのどこでNullPointerException発生するかを誰が知っていますか?運がよければ、この関数を呼び出した直後でなければ、実際の問題を追跡するのは非常に困難です。
-unholysampler

5
@MatthewRead到達不能にすることを目的としたコードの実行は重大なバグでreturn nullありnull、このバグから戻る他のケースを区別できないため、バグをマスクする可能性が非常に高くなります。
CodesInChaos

4
さらに、一部の状況で関数がすでにnullを返し、この状況でもnullリターンを使用する場合、ユーザーは、nullリターンを取得するときに考えられる2つのシナリオを持っています。nullリターンはすでに「オブジェクトがなく、理由を指定していない」ことを意味するため、これは重要ではない場合があります。しかし、あいまいさを追加することはしばしば悪いことであり、バグは、すぐに「これが壊れている」ように見えるのではなく、最初は「特定の状況」のように扱われることを意味します。早く失敗します。
スティーブジェソップ

6

throw new RuntimeException("cannot reach here");

声明は、それがクリアになりPERSON、何が起こっているコードを読んでそう多くは、その後、例えばヌルを返す方が良いです。また、コードが予期しない方法で変更された場合のデバッグが容易になります。

しかし、rethrow(e)間違っているようです!だから、中にあなたの場合、私は、コードをリファクタリングすることは良いオプションだと思います。コードを整理する方法については、他の回答(コアダンプのベストが好きです)を参照してください。


4

コンベンションがあるかどうかはわかりません。

とにかく、別のトリックはそうすることです:

private <T> T rethrow(Exception exception) {
    // or whatever it actually does
    Log.e("Ouch! " + exception.getMessage());
    throw new CustomWrapperException(exception);
}

これを可能にする:

try {
     return translate(mailManagementService.retrieveUserMailConfiguration(id));
} catch (Exception e) {
     return rethrow(e);
}

そして、人工的な RuntimeExceptionもはやは必要ありません。rethrow実際に値を返すことはありませんが、今ではコンパイラーにとって十分です。

理論上は値を返します(メソッドシグネチャ)が、例外をスローするため、実際に値を返すことは免除されます。

ええ、それは奇妙に見えるかもしれませんが、再び-幻を投げます RuntimeExceptionこの世界では決して見られないヌルを返します-それは正確な美しさでもありません。

読みやすくするために、名前を変更rethrowして次のようにすることができます。

} catch (Exception e) {
     return nothingJustRethrow(e);
}

2

tryブロックを完全に削除する場合、rethrowまたはは必要ありませんthrow。このコードは元のコードとまったく同じことを行います。

public Configuration retrieveUserMailConfiguration(Long id) throws MailException {
    return translate(mailManagementService.retrieveUserMailConfiguration(id));
}

それがよりベテランの開発者から来るという事実に惑わされないでください。これはコードの腐敗であり、常に発生します。修正してください。

編集:私rethrow(e)は単に例外を再スローすると誤解していますe。そのrethrowメソッドが実際に例外を再スローする以外の何かを行う場合、それを取り除くことはこのメソッドのセマンティクスを変更します。


何も処理しないワイルドカードキャッチは実際には例外ハンドラであると人々は信じ続けます。あなたのバージョンは、それができないことを処理するふりをしないことにより、その欠陥のある仮定を無視します。retrieveUserMailConfiguration()の呼び出し元は、何かを取り戻すことを期待しています。その関数が適切な値を返せない場合、高速かつ大音量で失敗するはずです。他の回答catch(Exception)では、いやらしいコード臭の問題は見られなかったかのようです。
msw

1
@msw-カーゴカルトコーディング。
ピートベッカー

@msw-一方、ブレークポイントを設定する場所を提供します。
ピートベッカー

1
ロジックのすべての部分を破棄しても問題がないように見えます。あなたのバージョンは明らかにオリジナルと同じことをしていません。私はあなたの答えのようなキャッチフリーメソッドを書くことを望みますが、既存のコードを扱うとき、他の作業部分との契約を破るべきではありません。で行われていることrethrowは役に立たないかもしれませんが(これはここではポイントではありません)、あなたが好きではないコードを取り除くだけではなく、明らかにそうでない場合は元のコードと同等であると言うことはできません。カスタムrethrow関数には副作用があります。
コアダンプ

1
@ jpmc26質問の核心は、「ここに到達できません」という例外に関するものです。これは、前のtry / catchブロック以外の理由で発生する可能性があります。しかし、私はブロックが魚臭いにおいがすることに同意し、この答えが元々より慎重に表現されていれば、それははるかに多くの賛成票を得たでしょう(私は反対票を取りませんでした)。最後の編集は良いです。
コアダンプ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.