はじめに
Java8 で作る REPL、その3になります。
前回の記事が想定以上の重さになってしまいましたので、今回は軽めのオブジェクトを作成します。
Finalizable インターフェース
Java を久しぶりに使ってみて、最初にえっと思ったこととして、デストラクタがないことにショックを受けました。
いわゆるRAII (リソース獲得は初期化である) を実装しようと思ったときに、デストラクタがなくて、どうやって実装したらいいの? となりました。
そこで、今回はREPL他で使用する、リソース獲得・開放を実現するためのクラスを作成します。
Java における RAII 実装方法
Javaにおけるリソース開放の実装方法について、あまり詳しくないので、ざっくりとまとめておきます。
Object.finalize() 非推奨
Object.finalize()
メソッドをオーバーライドすることで、ガベージコレクト時に任意の開放処理を記述することができます。
ただし、finalize()メソッドは、ガベージコレクタによってオブジェクトが破棄される時に実行される為、いつ実行されるかわかりません。特にメモリが潤沢な現在のPC環境では、アプリケーションが終了するまで、呼ばれない可能性もあります。
アプリケーションでただひとつしか使えず、かつ、開放処理後でなければ再利用できないようなリソースの場合(例は思い浮かびませんでした)、再利用前に開放処理が動く保証の無い、この方法は使用できません。
AutoCloseable.close()
AutoCloseable インターフェース実装クラスを作成し、try-with-resource ブロックを使用することで、tryブロック終了時に自動的に、実装した close() メソッドが呼ばれます。
一般的には、この形式が推奨されていますが、単一のメソッドの中で使用する必要があるため、例えばコンポジションオブジェクトを、親オブジェクト破棄のタイミングで破棄したい場合には使用できません。自前でコンポジションオブジェクトのclose()を呼び出す方法もありますが、本来の使用方法か? と聞かれれば疑問です。
try ( InputStreamReader in = new InputStreamReader(filepath) ) {
char c = in.read(); // in を使った処理
method(in); // 途中でメソッドを介して in を渡してもOK
// .. その他の処理
} // ここで in は自動的に開放
Finalizer (今回説明するやつ)
そこで、今回実装するのが、登録したオブジェクトの開放処理を行う Finalizer クラスです。
Finalizable インターフェース
終了処理を行いたいクラスに、実装させるインターフェースです。
Finlizer に、Finalizable のコレクションを保持させ、終了時に順次コレクション内のオブジェクトの cleanup()を呼び出す仕組みです。
package console;
interface Interface {
interface Finalizable {
void cleanup();
}
}
Finalizer クラス
こちらが、Finalizable実装クラスのコンテナ、兼、終了処理請負クラスです。
package console;
final class Finalizer {
/** Finalizable 実装クラス登録. */
final void register(Interface.Finalizable f) {
if ( finalizers_.contains(f) ) { return; }
finalizers_.add(0, f); // push front
}
final void remove(Interface.Finalizable f) {
if ( !finalizers_.contains(f) ) { return; }
finalizers_.remove(f);
}
/** 登録された Finalizable.cleanup() を登録順と逆の順序で呼び出し、消去する. */
final void cleanup() {
for ( Interface.Finalizable f: finalizers_ ) {
try {
f.cleanup();
} catch(java.lang.RuntimeException e) {
System.err.println( e.getMessage() );
}
}
finalizers_.clear();
finalizers_ = null;
}
private java.util.List<Interface.Finalizable> finalizers_ = new java.util.ArrayList<>();
}
-
register(Finalizable)
Finalizable 実装クラスを登録するメソッドです。
同一オブジェクトが既に登録済みの場合は、何も行いません。
また、最後に登録したオブジェクトをコンテナの最初に追加します。 -
remove()
登録された Finalizable 実装クラスを、除去するメソッドです。 -
cleanup()
コンテナ内のオブジェクトの cleanup() を順番に呼び出して行きます。
最後に登録したオブジェクトから cleanup() を呼び出します。これにより、依存関係のあるオブジェクトを登録していた場合であっても、正しく開放処理が行われます。
cleanup() 処理のループ内を try ~ catch ブロックで囲んでいますが、これは、万が一、先に外で cleanup() 処理が実行されたこといより、二重開放エラーとなった場合でも、残ったオブジェクトの cleanup() を実行する為です。
依存関係のあるオブジェクトの登録例
リソース開放を行いたい、Foo の初期化に Hogeが必要な場合、Hoge → Foo の順に Finlaizer に登録する。
こうすることで、Fooより先に、Hoge が開放されることは無い為、仮に Hoge.cleanup() の中で Foo を使用していたとしても問題は起きない。ただし、手動で Foo.cleanup()を呼び出したりしない限りは...。
Finalizer fin = new Finalizer();
Hoge h = new Hoge();
fin.register(h);
Foo f = new Foo(h);
fin.register(f);
// ... Hoge, Foo を使用した処理
fin.cleanup()
まとめ
Finalizer.cleanup() こそ、オブジェクト破棄時に自前で呼び出す必要がありますが、ここに登録すれば、一括してリソース開放してくれる為、きちんと実装しておけば、リソース漏れの懸念はかなり解消できると思います。
第1回の BaseREPL クラスに、Finalizerクラスにあるメソッドと同名の cleanup() メソッドが抽象クラスとして登録されています。派生クラスで、Finalizer を使用する場合は、このメソッドから Finalizer.cleanup() メソッドを呼べば、BaseREPL.run() メソッドの終了直前に登録しておいたリソース開放処理を確実に実行することができます。
class XxxxREPL extends BaseREPL {
XxxxREPL(String encoding) {
super(encoding);
obj1 = new Object1();
finalizer.register(obj1);
finalizer.register(obj2);
}
protected void cleanup() {
finalizer.cleanup(): // obj2、obj1 の順に cleanup() が呼ばれる
}
private Finalizer finalizer = new Finalizer();
private Object1 /* implements Finalizable */ obj1;
private Object2 /* implements Finalizable */ obj2 = new Object2();
}