4

次のような例を見てください。

public List<CloseableThing> readThings(List<File> files) throws IOException {
    ImmutableList.Builder<CloseableThing> things = ImmutableList.builder();
    try {
        for (File file : files) {
            things.add(readThing(file))
        }
        return things.build();
    } catch (Throwable t) {
        for (CloseableThing thing : things.build()) {
            thing.close();
        }
        throw t;
    }
}

一般的にThrowableをキャッチしないというルールがあるため、コードレビューのコメントが入りました。この種の障害のみのクリーンアップを行うための古いパターンは次のとおりです。

public List<CloseableThing> readThings(List<File> files) throws IOException {
    ImmutableList.Builder<CloseableThing> things = ImmutableList.builder();
    boolean success = false;
    try {
        for (File file : files) {
            things.add(readThing(file))
        }
        success = true;
        return things.build();
    } finally {
        if (!success) {
            for (CloseableThing thing : things.build()) {
                thing.close();
            }
        }
    }
}

これは少し面倒で、Throwable をキャッチするのと違うのかどうか完全には理解できません。いずれの場合も、例外が伝播します。いずれの場合も、OutOfMemoryError が発生した可能性がある場合に、追加のコードが実行されています。

それで、最終的には本当に安全ですか?

4

4 に答える 4

8

ThrowableExceptionErrorの親型であるため、Throwableをキャッチするとは、Exception と Error の両方をキャッチすることを意味します。例外は回復できるもの ( IOExceptionなど) であり、エラーはより深刻なものであり、通常は簡単に回復できないもの ( ClassNotFoundErrorなど) であるため、自分が何をしているのかわからない限り、エラーをキャッチしてもあまり意味がありません。 .

于 2013-06-27T03:20:21.603 に答える
7

クリーンアップを実行するために Throwable をキャッチしても問題ありませんか?

一言で言えば…いいえ。

問題は、 をキャッチして再スローする場合、メソッドがスローすることThrowable宣言するThrowable必要があることです。これにより、メソッドを呼び出すすべてのものに問題が発生します。

  • 呼び出し元は、Throwable.
  • プログラマは、チェックされた例外が処理されていないというコンパイラ エラーの形で、コンパイラから何の助けも得られなくなりました。(「対処」するとThrowable、まだ処理されていないすべてのチェック済みおよび未チェックの例外が含まれます。)

この道をたどり始めるthrows Throwableと、病気のように呼び出し階層全体に広がります...


リソースを閉じる正しい方法は、 を使用するfinallyか、Java 7 以降でコーディングしている場合は、「リソースを使用して試す」を使用し、リソースを自動で閉じることができるようにすることです。

(あなたの例では、それは少し難しいですが、既存のクラスを拡張して、メソッドがすべてのリストメンバーを閉じる「クローズList可能なリスト」クラスを作成できます。close()


Java 7 以降では、キャッチされるチェック済み例外のみをスローするように囲んでいるメソッドを宣言することで問題を解決できることは事実です。ただし、Throwable をキャッチしてクリーンアップを行うことは、人々が期待するものではありません。人々はクリーンアップの条項を期待しています。finallyファンキーな方法で行うと、コードが読みにくくなります...そしてそれは「OK」ではありません。あなたの方法がより簡潔であっても。

さらに、あなたのバージョンはJava 6 以前ではコンパイルできません。


要するに、私はあなたのコードのレビュアーに同意します。

私が同意する唯一のことは、バージョンとfinallyバージョンが両方とも正しく実装されていると仮定して「安全」である場合です。(問題は、プログラマーがあなたの場合、それが安全であることを理解するためにもっと一生懸命考えなければならないことです...あなたがそれをコーディングした非慣用的な方法のためです。)

于 2013-06-27T03:34:39.287 に答える
1

これは私自身の質問に答える試みですが、実験と Java コンパイラから得られた結果を使用しているため、特に哲学などに取り組んでいるわけではありません。

以下は、catch-cleanup-and-rethrow のサンプル コードです。

public CompoundResource catchThrowable() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        return new CompoundResource(stream1, stream2);
    } catch (Throwable t) {
        if (stream2 != null) {
            stream2.close();
        }
        if (stream1 != null) {
            stream1.close();
        }
        throw t;
    }
}

これは、次のバイトコードにコンパイルされます。

public Exceptions$CompoundResource catchThrowable() throws java.lang.Exception;
  Code:
     0: aconst_null   
     1: astore_1      
     2: aconst_null   
     3: astore_2      
     4: new           #2                  // class java/io/FileInputStream
     7: dup           
     8: ldc           #3                  // String 1
    10: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    13: astore_1      
    14: new           #2                  // class java/io/FileInputStream
    17: dup           
    18: ldc           #5                  // String 2
    20: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    23: astore_2      
    24: new           #6                  // class Exceptions$CompoundResource
    27: dup           
    28: aload_0       
    29: aload_1       
    30: aload_2       
    31: invokespecial #7                  // Method Exceptions$CompoundResource."<init>":(LExceptions;Ljava/io/Closeable;Ljava/io/Closeable;)V
    34: areturn       
    35: astore_3      
    36: aload_2       
    37: ifnull        44
    40: aload_2       
    41: invokevirtual #9                  // Method java/io/InputStream.close:()V
    44: aload_1       
    45: ifnull        52
    48: aload_1       
    49: invokevirtual #9                  // Method java/io/InputStream.close:()V
    52: aload_3       
    53: athrow        
  Exception table:
     from    to  target type
         4    34    35   Class java/lang/Throwable

次は、check-for-failure-in-finally-and-cleanup のコードで、それ以外は同じセマンティクスです。

public CompoundResource finallyHack() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    boolean success = false;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        success = true;
        return new CompoundResource(stream1, stream2);
    } finally {
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
    }
}

これは次のようにコンパイルされます。

public Exceptions$CompoundResource finallyHack() throws java.lang.Exception;
  Code:
     0: aconst_null   
     1: astore_1      
     2: aconst_null   
     3: astore_2      
     4: iconst_0      
     5: istore_3      
     6: new           #2                  // class java/io/FileInputStream
     9: dup           
    10: ldc           #3                  // String 1
    12: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    15: astore_1      
    16: new           #2                  // class java/io/FileInputStream
    19: dup           
    20: ldc           #5                  // String 2
    22: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    25: astore_2      
    26: iconst_1      
    27: istore_3      
    28: new           #6                  // class Exceptions$CompoundResource
    31: dup           
    32: aload_0       
    33: aload_1       
    34: aload_2       
    35: invokespecial #7                  // Method Exceptions$CompoundResource."<init>":(LExceptions;Ljava/io/Closeable;Ljava/io/Closeable;)V
    38: astore        4
    40: iload_3       
    41: ifne          60
    44: aload_2       
    45: ifnull        52
    48: aload_2       
    49: invokevirtual #9                  // Method java/io/InputStream.close:()V
    52: aload_1       
    53: ifnull        60
    56: aload_1       
    57: invokevirtual #9                  // Method java/io/InputStream.close:()V
    60: aload         4
    62: areturn       
    63: astore        5
    65: iload_3       
    66: ifne          85
    69: aload_2       
    70: ifnull        77
    73: aload_2       
    74: invokevirtual #9                  // Method java/io/InputStream.close:()V
    77: aload_1       
    78: ifnull        85
    81: aload_1       
    82: invokevirtual #9                  // Method java/io/InputStream.close:()V
    85: aload         5
    87: athrow        
  Exception table:
     from    to  target type
         6    40    63   any
        63    65    63   any

ここで何が起こっているかを注意深く見ると、return の時点と catch ブロック内の両方で、finally ブロック全体を複製した場合と同じバイトコードが生成されているようです。つまり、次のように書いたようなものです。

public CompoundResource finallyHack() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    boolean success = false;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        success = true;
        CompoundResource result = new CompoundResource(stream1, stream2);
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
        return result;
    } catch (any t) {    // just invented this syntax, this won't compile
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
        throw t;
    }
}

誰かが実際にそのコードを書いたら、あなたは彼らを笑うでしょう。成功ブランチでは、成功は常に真であるため、実行されないコードの大部分が存在するため、実行されないバイトコードが生成され、クラス ファイルが肥大化するだけです。例外分岐では、成功は常に false であるため、実行する必要があることがわかっているクリーンアップを実行する前に、値に対して不要なチェックを実行しているため、クラス ファイルのサイズが増加します。

注意すべき最も重要なことは次のとおりです。

catch (Throwable)とソリューションの両方がfinally実際にすべての例外をキャッチします。

Throwableでは「後片付けをするために捕まえてもいいですか?」という質問に答える限り……。

まだよくわかりませんが、キャッチThrowableしてもダメなら使ってもダメなのはわかってfinallyいます。そして、finallyどちらもOKでなければ、何が残りますか?

于 2014-04-07T12:24:11.590 に答える