web-dev-qa-db-ja.com

try-with-resourcesブロックで複数の連鎖リソースを管理するための正しいイディオム?

Java 7try-with-resources構文(別名ARM block(Automatic Resource Management))は、AutoCloseableリソースを1つだけ使用する場合は素晴らしく、短く、簡単です。互いに依存する複数のリソース、たとえば、それをラップするFileWriterBufferedWriterを宣言する必要があります。もちろん、この質問は、これら2つの特定のクラスだけでなく、いくつかのAutoCloseableリソースがラップされる場合にも関係します。

私は次の3つの選択肢を思いつきました。

1)

私が見た素朴なイディオムは、ARM管理変数でトップレベルラッパーのみを宣言することです。

_static void printToFile1(String text, File file) {
    try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}
_

これは素晴らしく短いですが、壊れています。基になるFileWriterは変数で宣言されていないため、生成されたfinallyブロックで直接閉じられることはありません。ラッピングcloseBufferedWriterメソッドを介してのみ閉じられます。問題は、bwのコンストラクターから例外がスローされた場合、そのcloseは呼び出されないため、基になるFileWriter閉じられないです。

2)

_static void printToFile2(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
            BufferedWriter bw = new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}
_

ここでは、基になるリソースとラッピングリソースの両方がARM管理変数で宣言されているため、両方とも確実に閉じられますが、基になるfw.close()2回呼び出されます:not直接だけでなく、ラッピングbw.close()を通じても。

これは、CloseableのサブタイプであるAutoCloseableを実装するこれら2つの特定のクラスの問題ではありません。その契約では、closeの複数の呼び出しが許可されています。

このストリームを閉じて、それに関連付けられているすべてのシステムリソースを解放します。ストリームがすでに閉じている場合、このメソッドを呼び出しても効果はありません。

ただし、一般的なケースでは、AutoCloseableのみを実装する(Closeableではなく)リソースを使用できますが、これはcloseが複数回呼び出されることを保証しません。

Java.io.Closeableのcloseメソッドとは異なり、このcloseメソッドはべき等である必要はありません。つまり、このcloseメソッドを2回以上呼び出すと、目に見える副作用が発生する可能性があります。Closeable.closeは、2回以上呼び出された場合に効果がないことが必要です。ただし、このインターフェイスの実装者は、closeメソッドをべき等にすることを強くお勧めします。

3)

_static void printToFile3(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        BufferedWriter bw = new BufferedWriter(fw);
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
}
_

fwのみがクリーンアップが必要な実際のリソースを表すため、このバージョンは理論的に正しいはずです。 bw自体はリソースを保持せず、fwに委任するだけなので、基になるfwを閉じるだけで十分です。

一方で、構文は少し不規則であり、Eclipseは警告を発行しますが、これは誤報であると考えていますが、依然として対処する必要がある警告です。

リソースリーク:「bw」は閉じられません


それでは、どのアプローチを採用すべきでしょうか?または、正しいのイディオムを逃しましたか?

161
Natix

これが私の代替案です。

1)

try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
    bw.write(text);
}

私にとって、15年前の従来のC++からJavaに到達したことで一番良かったのは、プログラムを信頼できるということでした。残りのコードは最高の振る舞いとバラの匂いにあります。実際、BufferedWriterはここで例外をスローするかもしれません。たとえば、メモリ不足は珍しいことではありません。どっち Java.ioラッパークラスはコンストラクターからチェック例外をスローしますか?しません。そのようなあいまいな知識に依存している場合、コードの理解性はあまり良くありません。

「破壊」もあります。エラー条件が存在する場合は、削除が必要なファイル(表示されていないコード)にゴミをフラッシュしたくないでしょう。もちろん、ファイルを削除することもエラー処理として行うべき興味深い操作です。

通常、finallyブロックはできるだけ短く、信頼できるものにする必要があります。フラッシュを追加しても、この目標には役立ちません。多くのリリースで、JDKのバッファリングクラスの一部には、flush内のcloseからの例外により、装飾されたオブジェクトのcloseが呼び出されないというバグがありました。それはしばらくの間修正されていますが、他の実装から期待してください。

2)

try (
    FileWriter fw = new FileWriter(file);
    BufferedWriter bw = new BufferedWriter(fw)
) {
    bw.write(text);
}

暗黙的なfinallyブロックはまだフラッシュしていますが(closeが繰り返されるようになりました-これはデコレータを追加するにつれて悪化します)、構築は安全であり、最終的にブロックを暗示する必要があるので、flushはリソースの解放を妨げません。

3)

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
}

ここにバグがあります。する必要があります:

try (FileWriter fw = new FileWriter(file)) {
    BufferedWriter bw = new BufferedWriter(fw);
    bw.write(text);
    bw.flush();
}

実装が不十分なデコレータの中には、実際にはリソースであり、確実に閉じる必要があるものがあります。また、一部のストリームは特定の方法で閉じる必要があります(おそらく圧縮を行っており、終了するためにビットを書き込む必要があり、すべてをフラッシュすることはできません)。

評決

3は技術的に優れたソリューションですが、ソフトウェア開発の理由により2がより良い選択となります。ただし、try-with-resourceは依然として不十分な修正であるため、 Execute Around idiom を使用する必要があります。これはJava SE 8 。

74

最初のスタイルは Oracleが推奨 です。 BufferedWriterはチェックされた例外をスローしないため、例外がスローされた場合、プログラムはそこから回復することを期待されず、リソースの回復はほとんど意味がありません。

主に、スレッドが死んでもプログラムが継続しているスレッドで発生する可能性があるためです-たとえば、プログラムの残りを深刻に損なうほどの長さではない一時的なメモリ停止がありました。ただし、これはかなり限られたケースであり、リソースリークを発生させるほど頻繁に発生する場合は、リソースの試用が最も問題になります。

19

オプション4

リソースを変更して、可能であればAutoClosableではなくCloseableにします。コンストラクターを連鎖できるという事実は、リソースを2回閉じることは前代未聞ではないことを意味します。 (これは、ARMも前に当てはまりました。)これについては、以下で詳しく説明します。

オプション5

ARMを使用しないでください。また、close()が2回呼び出されないように慎重にコードを作成してください!

オプション6

ARMを使用せず、最終的にclose()呼び出しをtry/catch自体で行わないでください。

この問題がARMに固有のものではないと思う理由

これらのすべての例で、finally close()呼び出しはcatchブロック内にある必要があります。読みやすくするために省略しました。

Fwを2回閉じることができるため、ダメです。 (FileWriterには問題ありませんが、仮説の例にはありません):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( fw != null ) fw.close();
  if ( bw != null ) bw.close();
}

BufferedWriterの構築で例外が発生した場合、fwが閉じられないため、良くありません。 (繰り返しますが、仮定の例では起こりえません):

FileWriter fw = null;
BufferedWriter bw = null;
try {
  fw = new FileWriter(file);
  bw = new BufferedWriter(fw);
  bw.write(text);
} finally {
  if ( bw != null ) bw.close();
}
5
Jeanne Boyarsky

以前のコメントに同意するには:最も簡単なのは(2)であり、Closeableリソースを使用して、try-with-resources句で順番に宣言します。 AutoCloseableしかない場合は、closeが1回だけ呼び出されることを確認するだけの別の(ネストされた)クラスにラップできます(Facadeパターン)。 private bool isClosed;。実際には、Oracleでさえ(1)コンストラクターをチェーンし、チェーンの途中で例外を正しく処理しません。

または、静的ファクトリーメソッドを使用して、連鎖リソースを手動で作成できます。これはチェーンをカプセル化し、途中で失敗した場合のクリーンアップを処理します。

static BufferedWriter createBufferedWriterFromFile(File file)
  throws IOException {
  // If constructor throws an exception, no resource acquired, so no release required.
  FileWriter fileWriter = new FileWriter(file);
  try {
    return new BufferedWriter(fileWriter);  
  } catch (IOException newBufferedWriterException) {
    try {
      fileWriter.close();
    } catch (IOException closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newBufferedWriterException.addSuppressed(closeException);
    }
    throw newBufferedWriterException;
  }
}

その後、try-with-resources句で単一のリソースとして使用できます。

try (BufferedWriter writer = createBufferedWriterFromFile(file)) {
  // Work with writer.
}

複雑なのは、複数の例外を処理することです。それ以外の場合は、「これまでに取得したリソースを閉じる」だけです。一般的な方法は、最初にリソースを保持するオブジェクトを保持する変数をnull(ここではfileWriter)に初期化し、クリーンアップにnullチェックを含めることですが、それは不要なようです:コンストラクターが失敗した場合、クリーンアップするものは何もないので、その例外を伝播させるだけで、コードが少し単純化されます。

おそらくこれを一般的に行うことができます:

static <T extends AutoCloseable, U extends AutoCloseable, V>
    T createChainedResource(V v) throws Exception {
  // If constructor throws an exception, no resource acquired, so no release required.
  U u = new U(v);
  try {
    return new T(u);  
  } catch (Exception newTException) {
    try {
      u.close();
    } catch (Exception closeException) {
      // Exceptions in cleanup code are secondary to exceptions in primary code (body of try),
      // as in try-with-resources.
      newTException.addSuppressed(closeException);
    }
    throw newTException;
  }
}

同様に、3つのリソースなどをチェーンできます。

数学的には、一度に2つのリソースをチェーンすることで3回チェーンすることもできます。これは連想的です。つまり、成功すると同じオブジェクト(コンストラクターが連想的であるため)を取得し、失敗した場合は同じ例外を取得しますコンストラクタのいずれか。上記のチェーンに[〜#〜] s [〜#〜]を追加したと仮定します(つまり、[〜#〜] v [〜#〜]で終わり、[〜#〜] s [〜#〜][〜#〜] u [〜#〜][〜# 〜] t [〜#〜]、および[〜#〜] s [〜#〜] )、最初のチェーン[〜#〜] s [〜#〜]および [〜#〜] t [〜#〜]、次に[〜#〜] u [〜#〜](ST)Uに対応、または最初に連鎖した場合[〜#〜] t [〜# 〜]および[〜#〜] u [〜#〜]、その後[〜#〜] s [〜#〜]S(TU)に対応。ただし、単一のファクトリ関数で明示的な3重チェーンを記述するだけの方が明確です。

3
Nils von Barth

ARMを使用せず、FileWriterが常に1回だけ閉じられるようにするという、Jeanne Boyarskyの提案に基づいて構築したかっただけです。

FileWriter fw = null;
BufferedWriter bw = null;
try {
    fw = new FileWriter(file);
    bw = new BufferedWriter(fw);
    bw.write(text);
} finally {
    if (bw != null) bw.close();
    else if (fw != null) fw.close();
}

ARMは単なる構文上の砂糖であるため、finallyブロックを置き換えるために常に使用できるとは限りません。for-eachループを使用して、イテレータ。

3
AmyGamy

リソースはネストされているため、try-with句も次のようにする必要があります。

try (FileWriter fw=new FileWriter(file)) {
    try (BufferedWriter bw=new BufferedWriter(fw)) {
        bw.write(text);
    } catch (IOException ex) {
        // handle ex
    }
} catch (IOException ex) {
    // handle ex
}
2
poison

ARMを使用せず、Closeableに進みます。次のようなメソッドを使用します。

public void close(Closeable... closeables) {
    for (Closeable closeable: closeables) {
       try {
           closeable.close();
         } catch (IOException e) {
           // you can't much for this
          }
    }

}

また、BufferedWriterに近いものを委任するだけでなく、FileWriterのようなクリーンアップを行うため、flushBufferのc​​loseの呼び出しを検討する必要があります。

0
sakthisundar

私の解決策は、次のように「抽出メソッド」リファクタリングを行うことです。

_static AutoCloseable writeFileWriter(FileWriter fw, String txt) throws IOException{
    final BufferedWriter bw  = new BufferedWriter(fw);
    bw.write(txt);
    return new AutoCloseable(){

        @Override
        public void close() throws IOException {
            bw.flush();
        }

    };
}
_

printToFileは次のいずれかで記述できます

_static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file)) {
        AutoCloseable w = writeFileWriter(fw, text);
        w.close();
    } catch (Exception ex) {
        // handle ex
    }
}
_

または

_static void printToFile(String text, File file) {
    try (FileWriter fw = new FileWriter(file);
        AutoCloseable w = writeFileWriter(fw, text)){

    } catch (Exception ex) {
        // handle ex
    }
}
_

クラスライブラリデザイナーの場合は、AutoClosableインターフェイスを追加メソッドで拡張して、閉じることを抑制することをお勧めします。この場合、手動で閉じる動作を制御できます。

言語設計者にとっての教訓は、新しい機能を追加することは他の多くの機能を追加することを意味する可能性があるということです。この場合、Javaの場合、明らかにARM機能は、リソース所有権の転送メカニズムを使用するとより適切に機能します。

[〜#〜] update [〜#〜]

関数内のBufferedWriterclose()を必要とするため、上記のコードは元々_@SuppressWarning_を必要とします。

コメントで示唆されているように、ライターを閉じる前にflush()を呼び出す場合は、tryブロック内のreturn(暗黙的または明示的)ステートメントの前に呼び出す必要があります。現在、呼び出し元がこれを実行していることを確認する方法がないため、writeFileWriterについて文書化する必要があります。

再度更新

上記の更新では、関数が呼び出し元にリソースを返す必要があるため、_@SuppressWarning_が不要になり、それ自体を閉じる必要はありません。残念ながら、これにより状況の最初に戻ります。警告は発信者側に戻ります。

したがって、これを適切に解決するには、カスタマイズされたAutoClosableが必要です。閉じると、下線BufferedWriterflush() edになります。実際、BufferWriterは決して閉じられないため、これは警告をバイパスする別の方法を示しています。

0
Earth Engine