Javaでリソースを適切に解放する

リソースの誤った解放は、Javaプログラマーの間で最もよく発生するエラーの1つです。 この記事のリソースでは、 java.io.Closeableインターフェースを実装するすべてのものを意味します。 だから、ポイントに右。

OutputStreamの例を検討します。 タスク:入力へのOutputStreamを取得し、いくつかの有用な作業を行い、 OutputStream閉じます。

間違った決定No. 1


 OutputStream stream = openOutputStream(); // -   stream stream.close(); 


コードで例外がスローされた場合、stream.close()は呼び出されないため、このソリューションは危険です。 リソースリークが発生します(接続が閉じられない、ファイル記述子が解放されない、など)

間違った決定No. 2


前のコードを修正してみましょう。 try-finallyを使用します。

 OutputStream stream = openOutputStream(); try { // -   stream } finally { stream.close(); } 


現在、 close()は常に呼び出されます( finally )ため:リソースはとにかく解放されます。 すべてが正しいようです。 そうですか?

いや

問題は次のとおりです。 close()メソッドは例外をスローする場合があります。 また、リソースを操作するためのメインコードも例外をスローする場合、 close()からの例外によってオーバーランされます。 元のエラーに関する情報は失われます。元の例外の原因はわかりません。

間違った決定番号3


状況を修正してみましょう。 stream.close()が「メイン」例外をキャッチできる場合、 close()から例外を「飲み込み」ましょう。

 OutputStream stream = openOutputStream(); try { // -   stream } finally { try { stream.close(); } catch (Throwable unused) { //  } } 


今、すべてがうまくいくようです。 私たちはお茶を飲みに行くことができます。

どんなに。 このソリューションは、前のソリューションよりもさらに悪いです。 なんで?

なぜなら、私たちはclose()から例外を取得して飲み込んだからです。 outputStreamBufferedOutputStreamラップされたFileOutputStreamとしoutputStreamう。 BufferedOutputStreamは、基礎となるストリームをチャンクでflush()するため、 close()呼び出し中に呼び出す可能性があります。 ここで、書き込み先のファイルがロックされていると想像してください。 その後、 close()メソッドはIOExceptionをスローし、これは正常に食べられます。 ユーザーデータの1バイトはファイルに書き込まれず、それについて何も学習しませんでした。 情報が失われました。

このソリューションを前のソリューションと比較すると、少なくとも何か悪いことが起こったことがわかります。 ここでは、すべてのエラー情報が消えます。

注: OutputStream代わりにInputStream使用されている場合、このコードには生存権があります。 事実は、例外がInputStream.close()でスローされた場合、このストリームから必要なものをすべて考慮したため、(ほとんどの場合)悪い結果はありません。 これは、 InputStreamOutputStreamセマンティクスがまったく異なることを意味します。

不完全なソリューション


では、リソース処理コードはどのように見えるのでしょうか?

メインコードが例外をスローする場合、この例外はclose()メソッドでスローできるものよりも優先される必要があることを考慮する必要があります。 次のようになります。

 OutputStream stream = openOutputStream(); Throwable mainThrowable = null; try { // -   stream } catch (Throwable t) { //   mainThrowable = t; //      throw t; } finally { if (mainThrowable == null) { //    .   close() stream.close(); } else { try { stream.close(); } catch (Throwable unused) { // ,      //     ( ) } } } 


この決定の欠点は明らかです。面倒で難しいです。 また、メインコードが例外をスローすると、 close()からの例外に関する情報close()失われます。 また、 openOutputStream()nullを返すことがあり、その後NullPointerExceptionNullPointerExceptionされます(別のifを追加することで解決され、さらに面倒なコードになります)。 最後に、2つのリソース( InputStreamOutputStream )がある場合、コードは単純に耐えられないほど複雑になります。

適切なソリューション(Java 7)


Java 7では、 try-with-resourcesコンストラクトが導入されました。 私たちはそれを使用します:

 try (OutputStream stream = openOutputStream()) { // -   stream } 


それだけです。

メインコードとclose()メソッドで例外がスローされた場合、最初の例外が優先され、2番目の例外が抑制されますが、その情報は保存されます(Javaコンパイラによって暗黙的に呼び出されるThrowable.addSuppressed(Throwable exception)メソッドを使用):

 Exception in thread "main" java.lang.RuntimeException: Main exception at A$1.write(A.java:16) at A.doSomething(A.java:27) at A.main(A.java:8) Suppressed: java.lang.RuntimeException: Exception on close() at A$1.close(A.java:21) at A.main(A.java:9) 


適切なソリューション(Google Guavaを使用したJava 6)


Java 6では、標準ライブラリだけでは対応できません。 しかし、素晴らしいGoogle Guavaライブラリが私たちの助けになります。 クラスcom.google.common.io.Closer (貧しい人々のためのtry-with-resources )がGuava 14.0に登場しました。これにより、上記の不完全なソリューションを大幅に簡素化できます。

 Closer closer = Closer.create(); try { OutputStream stream = closer.register(openOutputStream()); // -   stream } catch (Throwable e) { //     (  Error') throw closer.rethrow(e); } finally { closer.close(); } 


このソリューションは、Java 7の場合よりも著しく長くなりますが、不完全なソリューションよりもはるかに短くなります。 出力はJava 7とほぼ同じになります。

Closerは、その中の任意の量のリソースもサポートします( register(...)メソッド)。 残念ながら、 Closer@Betaアノテーションでマークされたクラスです。つまり、ライブラリの将来のバージョンで大幅に変更される可能性があります(削除まで)。

結論


リソースを正しく解放するのは、見た目ほど簡単ではありません(Java 7のみ)。 これには常に十分な注意を払ってください。 InputStreamOutputStreamReaderWriter )の処理は異なります(少なくともJava 6では)。

追加/修正は大歓迎です!

次回はNullPointerException処理方法を説明する予定です。

Source: https://habr.com/ru/post/J178405/


All Articles