前回の 記事 (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