Javaのfinalizeメソッドの手引き

Javaのfinalizeメソッドのガイド

1. 概要

このチュートリアルでは、Java言語のコアな側面であるルートObjectクラスによって提供されるfinalizeメソッドに焦点を当てます。

簡単に言えば、これは特定のオブジェクトのガベージコレクションの前に呼び出されます。

2. ファイナライザーの使用

finalize()メソッドはファイナライザーと呼ばれます。

JVMがこの特定のインスタンスをガベージコレクションする必要があると判断すると、ファイナライザが呼び出されます。 このようなファイナライザは、オブジェクトを元の状態に戻すなど、あらゆる操作を実行できます。

ただし、ファイナライザーの主な目的は、オブジェクトがメモリから削除される前に、オブジェクトによって使用されているリソースを解放することです。 ファイナライザは、クリーンアップ操作の主要なメカニズムとして、または他の方法が失敗した場合のセーフティネットとして機能します。

ファイナライザーがどのように機能するかを理解するために、クラス宣言を見てみましょう。

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

クラスFinalizableには、クローズ可能なリソースを参照するフィールドreaderがあります。 このクラスからオブジェクトが作成されると、クラスパス内のファイルから読み取る新しいBufferedReaderインスタンスが作成されます。

このようなインスタンスは、readFirstLineメソッドで使用され、指定されたファイルの最初の行を抽出します。 Notice that the reader isn’t closed in the given code.

ファイナライザーを使用してそれを行うことができます。

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

ファイナライザーが通常のインスタンスメソッドと同じように宣言されていることは簡単にわかります。

実際には、the time at which the garbage collector calls finalizers is dependent on the JVM’s implementation and the system’s conditions, which are out of our control.

ガベージコレクションをその場で実行するために、System.gcメソッドを利用します。 現実のシステムでは、いくつかの理由により、明示的に呼び出さないでください。

  1. 費用がかかります

  2. ガベージコレクションがすぐにトリガーされるわけではありません。JVMがGCを開始するためのヒントにすぎません。

  3. JVMはGCを呼び出す必要がある場合によく知っている

GCを強制する必要がある場合は、jconsoleを使用できます。

以下は、ファイナライザの動作を示すテストケースです。

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("example.com", firstLine);
    System.gc();
}

最初のステートメントでは、Finalizableオブジェクトが作成され、次にそのreadFirstLineメソッドが呼び出されます。 このオブジェクトはどの変数にも割り当てられていないため、System.gcメソッドが呼び出されたときにガベージコレクションの対象になります。

テストのアサーションは入力ファイルの内容を検証し、カスタムクラスが期待どおりに動作することを証明するためにのみ使用されます。

提供されたテストを実行すると、ファイナライザで閉じられているバッファ付きリーダーに関するメッセージがコンソールに出力されます。 これは、finalizeメソッドが呼び出され、リソースがクリーンアップされたことを意味します。

この時点まで、ファイナライザは操作を事前に破棄するための優れた方法のように見えます。 しかし、それは完全に真実ではありません。

次のセクションでは、それらの使用を避ける必要がある理由を説明します。

3. ファイナライザーの回避

ファイナライザーを使用して重要なアクションを実行するときに直面するいくつかの問題を見てみましょう。

ファイナライザに関連する最初の顕著な問題は、迅速性の欠如です。 ガベージコレクションはいつでも発生する可能性があるため、ファイナライザがいつ実行されるかはわかりません。

遅かれ早かれ、ファイナライザーがまだ呼び出されていることが最も重要であるため、これ自体は問題ではありません。 ただし、システムリソースは限られています。 Thus, we may run out of those resources before they get a chance to be cleaned up, potentially resulting in system crashes.

ファイナライザーは、プログラムの移植性にも影響を与えます。 ガベージコレクションアルゴリズムはJVM実装に依存しているため、プログラムは1つのシステムで非常に良好に動作し、実行時に別のシステムで異なる動作をする場合があります。

ファイナライザに伴うもう1つの重要な問題は、パフォーマンスコストです。 具体的には、JVM must perform much more operations when constructing and destroying objects containing a non-empty finalizerです。

詳細は実装固有ですが、一般的な考え方はすべてのJVMで同じです。オブジェクトが破棄される前にファイナライザーが実行されるようにするには、追加の手順を実行する必要があります。 これらの手順により、オブジェクトの作成と破棄の期間が数百または数千倍も長くなる可能性があります。

ここで説明する最後の問題は、ファイナライズ中の例外処理の欠如です。 通知なしのIf a finalizer throws an exception, the finalization process is canceled, and the exception is ignored, leaving the object in a corrupted state

4. ファイナライザーなしの例

同じ機能を提供するが、finalize()メソッドを使用しないソリューションを調べてみましょう。 以下の例は、ファイナライザーを置き換える唯一の方法ではないことに注意してください。

代わりに、重要なポイントを示すために使用されます。ファイナライザーを回避するのに役立つオプションが常にあります。

新しいクラスの宣言は次のとおりです。

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

新しいCloseableResourceクラスと以前のFinalizableクラスの唯一の違いは、ファイナライザー定義ではなく、AutoCloseableインターフェースの実装であることを理解するのは難しいことではありません。

CloseableResourcecloseメソッドの本体は、クラスFinalizableのファイナライザーの本体とほぼ同じであることに注意してください。

以下は、入力ファイルを読み取り、ジョブの終了後にリソースを解放するテストメソッドです。

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("example.com", firstLine);
    }
}

上記のテストでは、try-with-resourcesステートメントのtryブロックにCloseableResourceインスタンスが作成されるため、try-with-resourcesブロックの実行が完了すると、そのリソースは自動的に閉じられます。

指定されたテストメソッドを実行すると、CloseableResourceクラスのcloseメソッドから出力されたメッセージが表示されます。

5. 結論

このチュートリアルでは、Javaのコアコンセプトであるfinalizeメソッドに焦点を当てました。 これは紙上では便利に見えますが、実行時にい副作用を引き起こす可能性があります。 そして、さらに重要なことに、ファイナライザーを使用するための代替ソリューションが常にあります。

注意すべき重要な点の1つは、finalizeがJava 9以降非推奨になり、最終的には削除されることです。

いつものように、このチュートリアルのソースコードはover on GitHubにあります。