新しいコレクション
コレクションライブラリという名前から予想されるとおり、新しいコレクションがいくつか用意されています。
Multimap
再度、前述の例を考えます。これもリストへのマップですが、今度は少し複雑に複数の型を使います。
Map<Integer, List<String>> mapOfLists = Maps.newHashMap();
多分、これと同じようなものを書いたことがある人は少なくないはずです。実際、何かをリストにマップするという操作は、アイテムを集めて識別可能なバケットとして1つにまとめるという操作の共通パターンと見ることができます。このパターンを書いたことがある人なら、マップに新しいアイテムを追加するとき、どのようなコードを書く必要があるかご存じでしょう。
List<String> list = mapOfLists.get(key); if (list == null) { list = new ArrayList<String>(); mapOfLists.put(key, list); } list.add(value);
つまり、マップに値を追加するときは、その新しい値を受け取るリストを確実に用意しておく必要があるということです。抜かりのない人なら、アイテムを削除するときリストが空かどうかもチェックするはずで、リストが空ならリソースを節約する意味でもリスト自体を削除することでしょう。この種の定型パターンが多用される例を目にすることは、めったにありません。
Multimapは、これをサポートする実装(厳密には実装の集合)です。実際、ArrayLists、LinkedLists、Treesといった構造をベースとするMultimapが用意されています。
Multimap<Integer, String> numbers = Multimaps.newArrayListmultiMap(); numbers.put(1, "One"); numbers.put(1, "Uno"); numbers.put(2, "Two"); numbers.put(2, "Dos"); numbers.remove(1,"One"); numbers.removeAll(2);
Multimapをさらに突っ込んで調べると、これがとても強力で、もっと奥の深いものであることがわかります。たとえば、リストのマップがあるとき、それらのリストに含まれるすべての値から最大の値を求めようとすると、イテレーションやら比較やらのコードを大量に書く必要があります。Multimapを使うと、これをCollections.max(numbers.values())といった単純な形で書くことができます。このようにMultimapで時間と手間を省ける状況は、いくらでもあります。
Multiset
Multisetは、おそらくMultimapほど広くは使われないでしょうが、ヒストグラムやカウンタに利用できます。
Multiset<String> histogram = Multisets.newHashMultiSet(); histogram.add("Hello"); histogram.add("World", 3); histogram.add("Hello"); histogram.add("!"); int count; count = histogram.count("Hello"); // 2 count = histogram.count("World"); // 3 count = histogram.count("!"); // 1 count = histogram.count("Fred"); // 0
Multisetの実装では、ある効率的なカウンタの実装が内部的に用いられているため、たいていの操作において、MapやListを使うよりもずっと高い性能が得られます。Enums、Hashes、LinkedHashes、Treesといった構造をベースとする実装が用意されています。さらに、ほとんどロックを必要としない完全に並列処理の更新を実現するConcurrentMultisetまであります。
BiMap
BiMapは、双方向のマップ機能を実現します。双方向マップでは、キーと値が共に一意で、値からキーを引くことも可能です。次に例を示します。
BiMap<Integer, String> numbers = Maps.newHashBiMap(); numbers.put(1, "one"); numbers.put(2, "two"); numbers.put(3, "three"); numbers.put(4, "four"); numbers.put(5, "five"); String one = numbers.get(1); // one int three = numbers.inverse().get("three"); // three Integer nine = numbers.inverse().get("nine"); // null // NullPointerException int oops = numbers.inverse().get("nine");
別にGoogle Collections Libraryに限った話でありませんが、このような表面的に見えない基本型の変換には警戒が必要です。この例のget("three")コールは問題なく動きます。しかし、get("nine")行でnullポインタの例外が発生します。存在しないキーや値を探すようBiMapに要求することは(通常のマップと同様に)正当な操作であり、当然nullが返されることを想定すべきです。そのIntegerをコンパイラがint型に変換しようとしたときNullPointerExceptionが発生します。
PrimitiveArrays
Javaは、Objectの下に展開されるオブジェクト指向の階層構造だけでなく、基本型もサポートしています。これはJavaでよく議論される問題点の1つでもあります。すなわち、効率を考えると基本型を積極的に使うべきで、コレクションはオブジェクトの中でのみ扱うべきだという主張です。
基本型の自動変換機能が追加されたのはJava 5のときなので、コレクションが本当に基本型を受け取って返しているように見せることも不可能ではありません(無論、前述のように基本型の変換に際してNullPointerExceptionが発生する可能性があることを承知した上での話ですが)。しかし、実際には舞台裏で個々の基本型はオブジェクト型に変換されてからコレクションに追加されます。これは、状況によっては内部表現が最適でなくなることを意味します。
たとえば、100万個の基本型データを含む配列があって、それを(配列でなくコレクションを要求するメソッドか何かのパラメータとして使うために)コレクションとして扱いたいことがあります。あるいは、そのデータがたまにしかアクセスされないことがわかっているのに(たとえば、10個かそこらの要素がコレクションからランダムに取り出される)、その10個がどう選ばれるかわからないこともあります。
このような状況で基本型の配列からコレクションを作成しても、やたら遅くて、メモリを消費するばかりです。
int[] intArray = new int[1000000]; // Assume it has some data put in it here... List<Integer> intList = Lists.newArrayList(1000000); for (int i : intArray) { intList.add(i); } System.out.println(intList.get(5)); System.out.println(intList.get(707070));
これでもまあ動きますが、正直冗談のような話です。
そこで、次のような方法が考えられます。
int[] intArray = new int[1000000]; List<Integer> intList = PrimitiveArrays.asList(intArray); System.out.println(intList.get(5)); System.out.println(intList.get(707070));
この例で、intListに保持される実装はオリジナルの配列をそのまま保持し、その値はそこから取り出されるときだけオブジェクト型に変換されます。このコレクション内に値を設定することもできますが、渡す値がnullであってはならない(オブジェクトの配列ではそのようなことが起こり得る)という制約が課せられます。
PrimitiveArraysは、基本型の大きな配列がたまにしかアクセスされないような状況で使うものであり、小さな配列が何度もアクセスされるような状況では、アクセスのたびに基本型の自動変換のオーバーヘッドが生じるので、これを使うべきでありません。
ReferenceMap
ReferenceMapはかなり特殊なクラスでありながら利用価値がとても高く、キャッシュを実装しようと考えている人にうってつけの機能です。
キャッシュ(弱参照マップとも呼ばれます)は、アプリケーションの負荷を簡単に切り詰めることができるようにする仕組みです。データベースのような低速の、あるいは高コストの情報源から同じアイテムを何度も取り出さず、必要なアイテムをいったん見つけたら、それをWeakHashMapに格納し、それ以降の検索では、WeakHashMap内に一致するものが既にあるかどうか調べるというものです。WeakHashMap内のキーに対する参照はいずれも弱参照であるため、ガーベジコレクションのとき、そのキーに対する強参照がなければ、その値がヒープから削除されて領域も回収されるため、マップ内のアイテムは直ちに消失します。
ReferenceMapは、WeakHashMapよりもやや強力で、キーの参照を弱参照に限定せず、弱参照、ソフト参照、強参照の任意の組み合わせをマップのキーと値に許すことで、考え得るあらゆるキャッシュの実装要求に対応できるようにするというものです。
さらにConcurrentMapもベースとなっているため、並列処理システムでの用途にも最適です。実際、ReferenceMapのStrong:Strongインスタンスは意味論的にConcurrentMapと同一であり、Weak:Strongインスタンスは実質的にWeakHashMapと差し替えることができます。これ以外の組み合わせも自由に使うことができ、キーか値を再利用する場合は、アイテムがReferenceMapから削除されます。
ソフト参照もあります。ソフト参照の仕様は、そもそもかなりソフトなものですが、弱参照よりやや強めを狙っています。弱参照の場合、ガーベジコレクションが起こると、強参照がない限りオブジェクトは削除されます。一方、ソフト参照の場合は、リソースが必要とされない限り、オブジェクトは保持されます。つまり、ヒープ領域が不足し始めるまでガーベジコレクションは起こらず、オブジェクトはできるだけ長くキャッシュにとどまろうとします。しかし、これは当のVMの実装に依存し、VMがソフト参照もかなり熱心に回収しているように見えることが結構あります。
以下は初期化の例です。
new ReferenceMap<String,
Integer>(ReferenceType.WEAK, ReferenceType.SOFT);
