Thursday, October 10, 2013

ListResourceBundle は注意して使ったほうがいいのかな

前回の 記事 (Oct 7, 2013) の続きです. ListResourceBundle というものの存在を知って, これはおもしろそうだなあと思ったまではいいけれど, 少しさわってみたりしていますと, いろいろ気をつけないといけない点もありそうだな, というのを感じるようになってきました.

どんな型の値が返ってくるかわからない場合

まず, 次のようなリソースバンドルクラスを用意してみます.

package tt4cs.resource;

import java.util.ListResourceBundle;

public class ProgressiveRock extends ListResourceBundle {
    
    @Override
    protected Object[][] getContents() {
        return new Object[][] {
            new Object[] {
                "ELP",
                new String[] { "Keith Emerson", "Greg Lake", "Carl Palmer" }
            }
        };
    }
    
}

このリソースでは, "ELP" というキーに対応する値は, "Keith Emerson", "Greg Lake", "Carl Palmer" という 3 個の要素からなる String 配列です.

もし, あらかじめ, "ELP" に対しては String 配列が返ってくるのだということを知らなければ, うっかり次のようなクライアントコードを書いてしまうかもしれません.

        ResourceBundle resource = ResourceBundle
                .getBundle(ProgressiveRock.class.getName());
        String elp = resource.getString("ELP");

この場合は, Javadoc にも書いてありますが, ClassCastException がスローされることになります.

これに対して, ClassCastException をスローするんではなく, たとえば配列の 1 番目の要素 (この例では "Keith Emerson") だけを返すようにするだとか, あるいは Arrays.toString(Object[]) を使って単一の文字列表現に変換したもの (この例では "[Keith Emerson, Greg Lake, Carl Palmer]") を返すようにするだとか, そんな代替策を, getString(String) メソッドをオーバーライドすることによって講じてみたくもなりそうです. しかしながら, そのメソッドは, final であると宣言されているので, オーバーライドすることができません.

そうなると, リソースバンドルを利用する側としては, どういう型のオブジェクトが返ってくるか確信が持てない場合には, getString(String) メソッドではなく, できるだけ getObject(String) メソッドを使うほうがよいのかもしれません. で, 返ってきたオブジェクトの型を instanceof か何かで確認し, そのうえで適切な型にキャストして利用するとか, そういうことになりましょうか.

不変なオブジェクトでない場合

先ほどと同じ例で考えます. 繰り返しになりますが, "ELP" というキーに対しては, 配列が返ってきます. 配列なので, ひょっとしたら, 変更ができてしまいそうです.

ためしに, 次のようなコードを書いてみます.

        ResourceBundle resource = ResourceBundle
                .getBundle(ProgressiveRock.class.getName());
        String[] elp = (String[]) resource.getObject("ELP");
        System.out.println(Arrays.toString(elp));
        elp[2] = "Cozy Powell";
        elp = (String[]) resource.getObject("ELP");
        System.out.println(Arrays.toString(elp));

そして, 実行してみますと, 次のような結果を得ます.

[Keith Emerson, Greg Lake, Carl Palmer]
[Keith Emerson, Greg Lake, Cozy Powell]

危惧したとおりです.

getContents() メソッド自体は, 呼ばれるたびに毎回 String 配列を作成しています. にもかかわらず, そのように変更ができてしまうということは, おそらく getContents() メソッドは一度だけ呼ばれ, その後は String 配列はどこかにキャッシュされるのでしょう.

キャッシュといえば, キャッシュをクリアする方法があるよなあと思いまして, 次のように ResourceBundle.Control クラスをためしてみたり,

        ResourceBundle.Control control = new ResourceBundle.Control() {
            @Override
            public long getTimeToLive(String baseName, Locale locale) {
                return ResourceBundle.Control.TTL_DONT_CACHE;
            }
        };
        ResourceBundle resource = ResourceBundle
                .getBundle(ProgressiveRock.class.getName(), control);

あるいは, 次のように clearCache() メソッドをためしてみたり,

        ResourceBundle.clearCache();

してみましたけれど, やはり変更後の値 (ここでの例では "Carl Palmer" ではなく "Cozy Powell") が適用されつづけてしまいます.

なぜキャッシュされたままなのだろうかと JDK のソースコードを読んでみましたところ, 要は ListResourceBundle クラスが lookup という名前のプライベートフィールド (型は Map<String, Object>) を持っていて, getContents() から得たリソースは, すべてそこに保存されてしまうのですね. ResourceBundle クラスのほうでいくらキャッシュ関連のチューニングをしても意味がなかったわけです.

こうなると, ListResourceBundle クラスを継承したクラスにおいては, getContents() を実装するだけでなく, たとえば handleGetObject(String) メソッドもオーバーライドしてしまおうか, という発想になるのですが, だめです, またしてもこのメソッドは final と宣言されていたのでした.

まとめ

前回は ListResourceBundle というものを知って, おもしろそうだなあと思ったのですが, 今回はいろいろ注意したほうがいい点が見えてきました. おそらく, プロパティファイルの代わりにリソースクラスというものをまじめに取り扱うなら, 既存の ListResourceBundle を継承するんではなく, 独自のリソースクラスを実装し, かつ ResourceBundle.Control も用意して, 独自リソースをバンドルする, といったことまで考えたほうがいいのかもしれません.

でも, いくらがんばったところで, プロパティファイルの手軽さには勝てないような気もしてきたりしていますけれど.

No comments:

Post a Comment