M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Eclipse SWTのdisposeメソッドについて

例によってJava関連であれこれ調べる中で、Eclipseで使用されていGUIツールキットStandard Widget ToolkitSWT)のdisposeメソッドが気になった。

f:id:m12i:20140831131630p:plain
SWT: Standard Widget Toolkit

Swingとは異なり、SWTでは各種ウィジェットでプラットフォーム固有の機能を利用するために、リソースの管理がJVMまかせにできない(Swingでも描画に関するAPIなど、ちょっと「深い」ところに行くと同じようなことにはなる)。

不要になったリソースはプログラムのビジネスロジック内で明示的に処分する必要があり、そのためにコールされるのがdisposeメソッド(もちろん親子関係を持つウィジェットでは上位オブジェクトのdisposeがカスケードされるのだが)。

このような面倒な事態が発生するのはJavaの言語仕様──Object#finalize()メソッドの仕様──に原因がある。ともあれこの仕様に関してはOCJ-P(SJC-P)などでも触れられる話題なので繰り返しはしない。

ともかく「それならばベスト・プラクティスはどうなるのか?」が気になって公式リファレンスの「When should you free?」のセクションを呼んでみた。その訳出メモをここに記す:

いつ解放すべきなのか?

それでは、SWTリソースをつくった後、いつそれを解放したらよいのでしょうか? まずはじめに、上述の理由〔訳注:思いっきり端折っているので必要な方は原典に当たってください〕の多くから、SWTリソースの管理にfinalizeメソッドを使用するのはおすすめできません。もしあなたが自身の作成したクラスのfinalizeメソッドをぜひとも実装すべきだと感じたとしても、それはあくまでも「セーフティ・ネット」や「補助的な仕組み」として考え、いずれにせよ〔訳注:finalizeに任せるのではなく、ビジネスロジック内で明示的に〕disposeメソッドを使用してください。UIスレッドのとの同期は忘れずに:

protected void finalize() {   // 非推奨です!
    super.finalize();
    display.asyncExec(new Runnable() {
            public void run() {
                    if (!myResource.isDisposed()) {
                    myResource.dispose();
                    }
            }
    });
}

それにしてもこのfinalizeメソッドによる「セーフティ・ネット」はおすすめできません。というのも、もしメモリ使用が上限に達したら、アプリケーションはこれらのオブジェクトからメモリが解放される前に、finalizeメソッドによる処理が終わるのを待たねばならなくなるからです。

SWTリソースを管理するのに最適な方法は、オブジェクトが不要になったらすぐdisposeメソッドを使うことです。いくつかのリソース種別──GCとPrinterのような──の場合、ほとんどいつもオブジェクトの作成とdisposeメソッドの呼び出しを同じメソッドのスコープ内で行うことになるでしょう。この方法はリソースが適切に処理されることを確かなものとする簡便な方法です。オブジェクトの作成とリソースの解放、この両方を確認するのにソースコード上の1カ所を見れば済むのですから。たとえば次のように:

        GC gc = new GC(canvas);
        gc.draw...   // GCを使いキャンバス上に何かしらの描画をする
        gc.dispose();

リソースが描画のためにGCにセットされている場合、それらのリソースがGCにセットされている間はdisposeメソッドを呼び出さないよう注意が必要です。リソースのdisposeを行う前にGCのdisposeを行ってください。たとえば次のように:

        Font font = new Font (display, "Courier", 10, SWT.NORMAL);
        GC gc = new GC(canvas);
        gc.setFont(font);
        gc.drawText...    // GCを使いキャンバス上に何かしらのテキストを描画する
        gc.dispose();
        font.dispose();

ペイントリスナにパラメータとして渡されたGCをdiposeしてはなりません。あなた自身が〔訳注:オブジェクトの作成を通して〕リソース割り当てをしたわけではないからです!:

        public void paintControl(PaintEvent event) {
            event.gc.draw...
            // event.gc.dispose()メソッドをコールしないこと!
        }

ウィジェットのなかで──たとえばwidget.setFont(font)といったメソッドを使用して──グラフィック・リソースを使用している場合、それらのリソースを処分する最適なタイミングはウィジェットがdisposeされるときです。この目的のためにウィジェットにdisposeリスナを仕込むことができます。たとえば次のように:

        widget.addDisposeListener(new DisposeListener() {
                public void widgetDisposed(DisposeEvent e) {
                font.dispose();
                }
        }


リソースが複数ウィジェット(やその他のオブジェクト)のあいだで共有されている場合、〔訳注:お手製の〕「リソース・マネージャ」クラスを用意して参照カウント方式の実装をする必要があるかもしれません。この機能はリソースへの参照の数を保持して、その数がゼロになったときにリソースを解放します。この場合、だいたい次のようにして、管理が必要なリソースごとにリソース・マネージャに問い合わせを行うことになるでしょう:

        Image image = myResourceManager.getNamedResource("imageName");

そしてウィジェットに設定するdisposeリスナは次のようになるはずです:

        widget.addDisposeListener(new DisposeListener() {
                public void widgetDisposed(DisposeEvent e) {
                myResourceManager.dispose(image);
                }
        });

参照カウント方式を使うか使わないか、そしてそれをどう実装するか、それはアプリケーションの基礎なる設計方針次第です。

ふつうプログラムのなかで明示的にウィジェットそれ自体をdisposeする必要はありません。shell〔訳注:ようするにウィンドウを構成するオブジェクト〕とその子要素たちは、ユーザがウィンドウを閉じた時点でdisposeされます。しかしshellを明示的にdisposeするのが必要な場面として典型的なものが2つあります:ユーザがアプリケーション・ウィンドウの[ファイル]→[閉じる]を選択した場合、そして、ダイアログで[OK]を選択したときです。[ファイル]→[閉じる]が選択された時のdisposeの例は次のようになります:

        // メニューバーを作成
        Menu menuBar = new Menu(shell, SWT.BAR);
        
        // [ファイル]メニュー項目を作成
        MenuItem item = new MenuItem(menuBar, SWT.CASCADE);
        item.setText("File");
        Menu fileMenu = new Menu(shell, SWT.DROP_DOWN);
        item.setMenu(fileMenu);
        
        // [ファイル]→[閉じる]メニュー項目を追加し、選択リスナを登録する
        item = new MenuItem(fileMenu, SWT.NULL);
        item.setText("Exit");
        item.addSelectionListener(new SelectionAdapter() {
                public void widgetSelected(SelectionEvent event) {
                shell.close(); // このメソッドのなかでdisposeメソッドが呼ばれる。下記注意を参照。
                }
        });

例の中ではdisposeメソッドではなくcloseメソッドがコールされていることに注意してください。これによりアプリケーションは必要に応じて終了処理をキャンセルすることができます(たとえば、何かしらの情報が保存前の状態であるようなケースです)。もちろんdisposeメソッドを呼び出すことで、shellリスナを起動することなしにshellの処分をすることもできますが、closeメソッドを呼び出すのがより良い方法です。