try-with-resourcesブロックで複数のチェーンされたリソースを管理するためのイディオムを修正しますか?


168

Java 7のtry-with-resources構文(ARMブロック(自動リソース管理)とも呼ばれます)は、AutoCloseableリソースを1つだけ使用する場合に便利で短く、簡単です。ただし、相互に依存する複数のリソースを宣言する必要がある場合、たとえばa FileWriterとa BufferedWriterをラップすることで、正しいイディオムが何であるかわかりません。もちろん、この質問は、AutoCloseableこれら2つの特定のクラスだけでなく、いくつかのリソースがラップされた場合のすべてのケースに関係します。

私は次の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ブロックで直接閉じられることはありません。closeラッピングの方法でのみ閉じられBufferedWriterます。問題は、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回呼び出されbw.close()ます。

これは、両方が実装するこれらの2つの特定のクラスCloseable(のサブタイプAutoCloseable)の問題にはなりませんclose

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

しかし、一般的なケースでは、私が唯一の実装リソース持つことができますAutoCloseable(とされていないCloseableことを保証するものではない)、close複数回呼び出すことができます。

java.io.Closeableのcloseメソッドとは異なり、このcloseメソッドはべき等である必要はないことに注意してください。つまり、このcloseメソッドを2回以上呼び出すと、目に見える副作用が発生する可能性があります。これは、2回以上呼び出すと効果がないことが必要なCloseable.closeとは異なります。ただし、このインターフェイスの実装者は、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'は決して閉じられません


それでは、どのアプローチを採用するのでしょうか。または、正しい他のイディオムを逃したことがありますか?


4
もちろん、基礎となるFileWriterのコンストラクターが例外をスローした場合、それは開かれず、すべて問題ありません。最初の例は、FileWriterが作成されたが、BufferedWriterのコンストラクターが例外をスローした場合にどうなるかを示しています。
Natix

6
BufferedWriterが例外をスローしないことは注目に値します。この質問が純粋な学術的ではない場所について考えることができる例はありますか?
Peter Lawrey、

10
@PeterLawreyはい、このシナリオでのBufferedWriterのコンストラクターはおそらく例外をスローしませんが、私が指摘したように、この質問はデコレータースタイルのリソースに関するものです。しかし、たとえば、public BufferedWriter(Writer out, int sz)をスローすることができIllegalArgumentExceptionます。また、BufferedWriterを拡張して、コンストラクターから何かをスローするクラスや、必要なカスタムラッパーを作成するクラスを追加することもできます。
Natix

5
BufferedWriterコンストラクタが簡単に例外をスローすることができます。OutOfMemoryErrorバッファにかなりの量のメモリを割り当てるため、おそらく最も一般的なものです(ただし、プロセス全体を再起動する必要があることを示す場合があります)。/あなたはする必要がありflush、あなたのBufferedWriterお近くにないと内容(一般的に維持したい場合のみ、非例外ケースを)。FileWriter「デフォルト」のファイルエンコーディングを取得します。明示的にすることをお勧めします。
トム・ホーティン-タックライン

10
@Natix私はSOのすべての質問がこの質問と同じように十分に研究され、明確であることを望みます。私はこれを100回賛成票に入れたいと思います。
オタク14

回答:


75

これが私の代替案です。

1)

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

私にとって、15年前に従来のC ++からJavaに到達した最高のことは、プログラムを信頼できることでした。物事が行き詰まって問題が発生している場合でも、多くの場合はそうですが、コードの残りの部分はバラの最高の動作と香りにしたいと思っています。確かに、BufferedWriterここで例外がスローされる可能性があります。たとえば、メモリ不足は珍しいことではありません。他のデコレータについては、どのjava.ioラッパークラスがコンストラクタからチェック例外をスローするか知っていますか?私はしません。そのようなあいまいな知識に依存している場合、コードの理解可能性はあまり良くありません。

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

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

2)

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

まだ暗黙のfinallyブロックでフラッシュしています(現在は繰り返さcloseれます-デコレーターを追加すると悪化します)。ただし、構築は安全であり、暗黙のfinallyブロックを実行する必要があるため、失敗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はまだ不十分な修正であり、Java SE 8でクロージャーを使用するより明確な構文を持つはずのExecute Aroundイディオムを使用する必要があります。


4
バージョン3では、bwがそのcloseを呼び出す必要がないことをどのようにして知っていますか?そして、それが確実であるとしても、バージョン1で言及したように、それも「あいまいな知識」ではないでしょうか。
TimK 2014年

3
ソフトウェア開発の理由により、2がより良い選択になります」このステートメントについて詳しく説明できますか?
Duncan Jones、

8
あなたは「閉鎖の周り実行イディオム」の例を与えることができます
マルクス

2
「Java SE 8のクロージャーを使用したより明確な構文」とは何ですか?
petertc

1
「イディオムをまわす実行」の例はここにある:stackoverflow.com/a/342016/258772
MRTS

20

最初のスタイルは、Oracleによって提案されたものです。BufferedWriterはチェック済み例外をスローしないため、例外がスローされても、プログラムはその例外からの回復を期待されないため、リソースの回復はほとんど無効になります。

ほとんどの場合、それはスレッドで発生する可能性があり、スレッドは停止しますが、プログラムはまだ継続しています。ただし、これはかなりまれなケースであり、リソースリークが問題になるほど頻繁に発生する場合は、try-with-resourcesが最も問題の少ないものです。


2
これは、Effective Javaの第3版でも推奨されるアプローチです。
shmosel

5

オプション4

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

オプション5

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

オプション6

ARMを使用せず、try / catchで最終的にclose()を呼び出します。

この問題が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();
}

3

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ループを使用できるわけではないのと同じように。


5
tryfinallyブロックの両方が例外をスローする場合、この構成は最初の(そして潜在的にはより有用な)構成を失います。
rxg 2013年

3

以前のコメントに同意するには:(2)Closeableリソースを使用して、try-with-resources句で順番に宣言するのが最も簡単です。しかない場合は、たとえばを使用することで、1回だけ呼び出されるAutoCloseableことをチェックするだけの別の(ネストされた)クラスclose(ファサードパターン)でそれらをラップできます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で終わる、UTSを順に適用する)と仮定すると、最初にSTをチェーンし、次にU(ST)Uに対応するか、最初にTUを連鎖させた場合、SS(TU)に対応します。ただし、1つのファクトリ関数で明示的な3重チェーンを書き込むだけの方が明確です。


のように、try-with-resourceを使用する必要があることを正しく収集していますtry (BufferedWriter writer = <BufferedWriter, FileWriter>createChainedResource(file)) { /* work with writer */ }か?
ErikE 2017年

@ErikEはい、まだtry-with-resourcesを使用する必要がありますが、チェーンされたリソースに対して単一の関数を使用するだけで済みます。ファクトリ関数がチェーンをカプセル化します。使用例を追加しました。ありがとう!
Nils von Barth 2017年

2

リソースはネストされているため、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
}

5
これは私の2番目の例とよく似ています。例外が発生しない場合、FileWriter closeは2回呼び出されます。
Natix 2012

0

ARMを使用せずに、Closeableを続行すると思います。次のような方法を使用します

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

}

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


0

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

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
    }
}

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

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

更新

もともと上記のコードは関数@SuppressWarningBufferedWriter内部で必要なのでclose()です。

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

もう一度更新

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

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


警告には意味があります。ここでbw実際にデータが書き出されることを確認できますか?それ結局バッファリングされるので、時々(バッファが満杯であるか、オンflush()であるか、close()メソッドである場合)、ディスクに書き込む必要があるだけです。flush()メソッドを呼び出す必要があると思います。しかし、1つのバッチですぐに書き出す場合は、とにかく、バッファ付きライターを使用する必要はありません。また、コードを修正しないと、ファイルに書き込まれたデータの順序が間違ったり、ファイルにまったく書き込まれなくなったりする可能性があります。
PetrJaneček2013

flush()呼び出す必要がある場合は、呼び出し元がを閉じることを決定する前に必ず発生しFileWriterます。そのため、これはtryブロックのsのprintToFile直前に発生するはずreturnです。これは一部でwriteFileWriterはないので、警告はその関数の内部ではなく、その関数の呼び出し側に関するものです。注釈がある@LiftWarningToCaller("wanrningXXX")場合、それはこのケースと同様のものを助けます。
Earth Engine
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.