|
「Google Collections Library」でJavaのコレクションを補完するはじめに何年も前のことですが、私が初めてJavaに興味を覚えた理由の1つは、Javaプラットフォームに標準でコレクションライブラリが組み込まれていたからでした。当時、C++の世界ではまだSTL(Standard Template Library)が定着しておらず、開発者たちは適当なコレクションライブラリを購入して利用するか(Rogue Waveが流行っていました)、自分の手でライブラリを書くしかありませんでした。正確な数は忘れましたが、私自身も、さまざまな目的でさまざまな種類のプリミティブやオブジェクトの連結リストを実装しました。さらに、もっと複雑なコレクションや平衡2分探索木、ハッシュテーブルなども自分で実装しました。そのようにしてソフトウェア工学の原理を絶えず意識することは決して無駄なことでありませんでしたが、生産性を考えるとそうとばかりも言えませんでした。しかし、Javaによって事態はまったく変わりました。バージョン1.0と1.1のコレクションクラスでも大きな改善が見られましたが、Java 1.2で導入されたJava Collections Frameworkは生産性を飛躍的に高めるものでした。以来、標準コレクションは拡張と改良を幾度となく繰り返し、Java 5で新たにジェネリックス(Generics)が導入されたことを受けて、(少なくともコンパイル時には)型検査をしたいと考える人々に好んで使われるようになりました。Doug Leaの並列処理コレクションの導入も歓迎されました。これがJava 5以降のJava.util.concurrentに組み込まれたことで、並列処理システムに適したQueueやConcurrentMapのようなコレクションを利用できるようになりました。 それにもかかわらず標準コレクションには欠点があります。アイテムがたびたび再実装され、ときにはとても最適とは言えないやり方が使われることもあります。また弱点もあります。ジェネリックス(少なくともJavaにおけるその実装)の費用対効果は目下議論されているところですが、好きか嫌いかは別としてジェネリックスが非常に冗長なものであることは間違いありません。たとえば、2003年当時はコレクションが次のようなスタイルで書かれていました。
Map mapOfLists = new HashMap();
Map<String, List<String>> mapOfLists =
new HashMap<String, List<String>>();
Google Collections Libraryは、GoogleがJavaコミュニティ向けに提供する新たなオープンソースライブラリです。Javaの現在のコレクションが抱える扱いにくさを漸進的に改善することと、独自のコレクションや機能を新たに付け加えることを目指しています。しかし同じ道を進むのはこれだけでなく、当然Apache Commons Collectionsとの比較も必要でしょう。コレクション増強ライブラリとしてどれを選ぶかは概して趣味の問題です。本稿では、Google Collections Libraryを取り上げることにします。私はこのライブラリを(内部的な形ながら)Googleのいくつかのプロジェクトで1年以上使ってみて、かなりよくできていると感じたからです。感触的にはJava Collections Frameworkが自然に発展したものと見ることができ、その高い信頼性と能力、そして何もかもが開示されている点を評価すると、今後のプロジェクトにこれなしで取り組むことなど到底考えられません。幸い、現在オープンソースプロジェクトとして提供されていることからすれば、そのような心配は無用のようです。 Google Collections Libraryを使う理由言うまでもなく、以下に挙げる理由は主観的なものです。それでも、Java Collections Frameworkのコレクション増強ライブラリとしてGoogle Collections Libraryを採用することには、それ相応の理由があります。
入手方法Google Collections Libraryは、http://code.google.com/p/google-collections/からダウンロードできます。本稿の執筆時点のバージョンは0.5です。実際、現時点でAPIの安定性について保証が得られていないので、APIに変更が生じる可能性があり、それによって本稿のサンプルが実態にそぐわなくなることも考えられます。しかし、生じる差異はわずかで、うまくすれば簡単に修正できるものと思われます。このライブラリをプロジェクトで実際に利用する手順は簡単で、ダウンロードしたアーカイブを解凍して、そこに含まれるjarファイルをインクルードするだけです。Javadocとソースファイルのアーカイブも提供されており、たいていの統合開発環境(IDE)では、ライブラリを定義するとき、それらがjarファイルに結び付けられます。これでライブラリをより簡単に使えるようになり、またプロジェクトのデバッグも容易になります。 簡単なサンプルまずは簡単な機能をいくつか見ていきましょう。簡単とは言っても、どれも私のお気に入りの機能です。最初に紹介するのは、「補助クリエータ」(Convenience Creators)です。前述のジェネリックスを用いた、何かをリストにマップする例をもう一度見てみましょう。
Map<String, List<String>> mapOfLists =
new HashMap<String, List<String>>();
Java 7に向けて提案されている型推論を適用すると、上の例は次のように書き換えられます。
Map<String, List<String>> mapOfLists =
new HashMap<>();
しかし、当座はGoogle Collections Libraryの別の機能を利用すれば、この冗長性を回避できます。 Map<String, List<String>> mapOfLists = Maps.newHashMap(); たとえば、
List<String> listOfStrings =
new ArrayList<String>();
List<String> listOfStrings = Lists.newArrayList(); String[] subdirs = new String[] {"usr", "local", "lib"}; String directory = Join.join(PATH_SEPARATOR, subdirs); 新しいコレクションコレクションライブラリという名前から予想されるとおり、新しいコレクションがいくつか用意されています。Multimap再度、前述の例を考えます。これもリストへのマップですが、今度は少し複雑に複数の型を使います。Map<Integer, List<String>> mapOfLists = Maps.newHashMap(); 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); Collections.max(numbers.values())といった単純な形で書くことができます。このようにMultimapで時間と手間を省ける状況は、いくらでもあります。MultisetMultisetは、おそらく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 BiMapBiMapは、双方向のマップ機能を実現します。双方向マップでは、キーと値が共に一意で、値からキーを引くことも可能です。次に例を示します。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"); get("three")コールは問題なく動きます。しかし、get("nine")行でnullポインタの例外が発生します。存在しないキーや値を探すようBiMapに要求することは(通常のマップと同様に)正当な操作であり、当然nullが返されることを想定すべきです。そのIntegerをコンパイラがint型に変換しようとしたときNullPointerExceptionが発生します。PrimitiveArraysJavaは、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)); PrimitiveArraysは、基本型の大きな配列がたまにしかアクセスされないような状況で使うものであり、小さな配列が何度もアクセスされるような状況では、アクセスのたびに基本型の自動変換のオーバーヘッドが生じるので、これを使うべきでありません。 ReferenceMapReferenceMapはかなり特殊なクラスでありながら利用価値がとても高く、キャッシュを実装しようと考えている人にうってつけの機能です。キャッシュ(弱参照マップとも呼ばれます)は、アプリケーションの負荷を簡単に切り詰めることができるようにする仕組みです。データベースのような低速の、あるいは高コストの情報源から同じアイテムを何度も取り出さず、必要なアイテムをいったん見つけたら、それをWeakHashMapに格納し、それ以降の検索では、WeakHashMap内に一致するものが既にあるかどうか調べるというものです。WeakHashMap内のキーに対する参照はいずれも弱参照であるため、ガーベジコレクションのとき、そのキーに対する強参照がなければ、その値がヒープから削除されて領域も回収されるため、マップ内のアイテムは直ちに消失します。 ReferenceMapは、WeakHashMapよりもやや強力で、キーの参照を弱参照に限定せず、弱参照、ソフト参照、強参照の任意の組み合わせをマップのキーと値に許すことで、考え得るあらゆるキャッシュの実装要求に対応できるようにするというものです。 さらにConcurrentMapもベースとなっているため、並列処理システムでの用途にも最適です。実際、ReferenceMapのStrong:Strongインスタンスは意味論的にConcurrentMapと同一であり、Weak:Strongインスタンスは実質的にWeakHashMapと差し替えることができます。これ以外の組み合わせも自由に使うことができ、キーか値を再利用する場合は、アイテムがReferenceMapから削除されます。 ソフト参照もあります。ソフト参照の仕様は、そもそもかなりソフトなものですが、弱参照よりやや強めを狙っています。弱参照の場合、ガーベジコレクションが起こると、強参照がない限りオブジェクトは削除されます。一方、ソフト参照の場合は、リソースが必要とされない限り、オブジェクトは保持されます。つまり、ヒープ領域が不足し始めるまでガーベジコレクションは起こらず、オブジェクトはできるだけ長くキャッシュにとどまろうとします。しかし、これは当のVMの実装に依存し、VMがソフト参照もかなり熱心に回収しているように見えることが結構あります。 以下は初期化の例です。
new ReferenceMap<String,
Integer>(ReferenceType.WEAK, ReferenceType.SOFT);
不変コレクションJava Collections Frameworkが開発された当時、変更不能なコレクションを返す仕組みが導入されましたが、これはコレクションの内容が誤って変更されないようにコレクションにラッパーをかぶせるというものでした。しかし、この変更不能コレクションには欠点があります。実は、変更できてしまうのです。変更不能コレクションを受け取った側が内容を変更できなくても、変更不能コレクションのベースとなるオリジナルのコレクションはまだ変更でき、両者がベースのコレクションを共有しているので、そのことが変更不能コレクションに影響します。 つまり、プログラマが変更不能コレクションを変更できないと決めてかかっても、それは正しくありません。 Google Collections Libraryでは、これに代わるものとして不変(immutable)コレクションが導入されています。オリジナルのコレクションから複製したコレクションを変更できないようにするので、これを受け取った側はコレクションが決して変更されないことを信頼することができます。 次に例を示します。 final List<String> immutableList = Lists.immutableList("Hello", "World"); // UnsupportedOperationException! immutableList.add("!"); 編集部注
最新版のライブラリでは、ListsからimmutableListメソッドが削除されています(2008/4/15現在)。以下同様。
JavaのunmodifiableListとGoogle Collections LibraryのimmutableListとでは、次のような違いが生じます。final List<String> baseList = Lists.newArrayList("Hello", "World"); final List<String> unmodifiableList = Collections.unmodifiableList(baseList); final List<String> immutableList = Lists.immutableList(baseList); baseList.add("!"); // modify the original list // prints [Hello, World, !] - changed System.out.println(unmodifiableList); // prints [Hello, World] - unchanged System.out.println(immutableList); 関数型プログラミング的な機能ここまで読まれた方は、Google Collections Libraryが、かなりまともでストレスを感じさせないコレクションだと知って、これからダウンロードしてみようと考えておられるかもしれません。しかし、ちょっと待ってください。まだ、嬉しいものがあります。Javaは関数型言語でありませんが、関数の考え方を少なくとも限定されたスコープで実に簡単に用いることができ、それによって可読性と効率を大きく高めることができます。 たとえば、自動車のリストがあり、それぞれの自動車は名前と価格を持つものと仮定します。このリストから価格の高い自動車をすべて抜き出して自動車のコレクションを作り、それをプログラムの中で調査することを考えます。 従来のやり方なら、自動車のコレクションをループで走査して価格をチェックし、価格が一定の限度を超えたら、その自動車に関する何らかの処理を実行するか、その自動車を別の新しい高価格自動車コレクションにコピーすることになるでしょう。 Google Collections Libraryには、これを別のやり方で実現するプレディケート(Predicate)という仕組みが用意されています。 プレディケート(Predicate)プレディケートとは、クラスのインスタンスに基づいて選択基準を指定し、そのクラスに対応するインスタンスをコレクションから選択的に抽出できるようにする仕組みです。次の例を見れば、この機能がよくわかるでしょう。final Predicate<Car> expensiveCar = new Predicate<Car>() { public boolean apply(Car car) { return car.price > 50000; } }; List<Car> cars = Lists.newArrayList(); cars.add(new Car("Ford Taurus", 20000)); cars.add(new Car("Tesla", 90000)); cars.add(new Car("Toyota Camry", 25000)); cars.add(new Car("McClaren F1", 600000)); final List<Car> premiumCars = Lists.immutableList(Iterables.filter(cars, expensiveCar)); Javaの内部クラスの構文のインパクトが薄れる面はありますが、関数的な処理としては、かなりエレガントです(プレディケートをインラインで定義しようとすると、無名内部クラスの構文を使う必要があります)。Java 7ではクロージャがいくつか提案されており、それを使えば、もっと簡潔に書けるでしょう。いずれにせよ、この簡単な例だけでははっきりしませんが、コードがもっと複雑になれば、ループを使った方法よりもずっと簡潔で可読性が高くなることがわかるはずです。 関数(Function)プレディケートがリストを抽出するのと同様に、関数はコレクション内の要素を変換して別の新しいコレクション(型が同じとは限らない)を作る仕組みを提供します。これは多くの関数型言語のマップ機能のようなものです。次に例を示します。 final Function<Integer, String> nameForNumber = new Function<Integer, String>() { public String apply(Integer from) { return numbers.get(from); } }; List<Integer> sequence = Lists.newArrayList(new Integer[] {1,2,3,5,3,1,4}); for (String name : Iterables.transform(sequence, nameForNumber)) { System.out.println(name); } この例では、Integerを対応するString名にマップするために、以前作成した双方向マップ(BiMap)のnumbersを使用しています。その後、Iterables.transform関数でIntegerのArrayListを変換して対応する名前を求めます。簡単で、しかもエレガントなやり方です。 この例から関数の効用はよくわかりますが、実際には同じことがもっと簡単にできます。Functionsクラスに静的な補助メソッドがいくつか定義されており、よく使う関数はそれらのメソッドで提供されます。たとえば、そのうちの1つであるforMap()は、マップを受け取り、そのマップをコレクションに適用する関数を作ります。したがって、これを上の例に当てはめると、Function内部クラスの定義を次のように書き換えることができます。 Function<Integer, String> nameForNumber = Functions.forMap(numbers); 制約(Constraint)このライブラリにおいて、関数的なプログラミングを実現するもう1つの仕組みは制約です。制約とはコレクションにどのような値を追加してよいかを追加的にコントロールするものです。このライブラリの制約は、まだまだ大きく変更されているので、ここで詳しい例を示すことはしません。これを使用した場合、Google Collections Libraryをアップグレードする際、それまでに作成したコードのアップデートが必要になる公算が大きくなります。ステータス0.5というバージョン番号に反して、このライブラリは信頼性も含めて相当なレベルまで仕上がっていますが、まだ完成していないことを肝に銘じてください。具体的に言えば、このライブラリはJava 5とJava 6のどちらでも動きますが、Java 6のすべての機能に対応しているわけではありません(たとえば、Navigableインターフェイスには対応していません)。これから登場するバージョンはJava 6の機能に対応したものとなりますが、Java 5に対応するバージョンは今後もJava 7が登場するまで引き続きメンテナンスされます。結論Google Collections Libraryを使うと、生産性が向上するのは言うまでもなく、コードの可読性が大きく改善される可能性があります。本稿が、Google Collections Libraryをプロジェクトで利用しようと考えている人にとって有用な判断材料となれば幸いです。このライブラリはオープンソースなので、リスクは比較的小さいですが、Java 7の規格が現時点でどちらに向かうかまだはっきりしない部分があるので、将来、規格に適合させるためにリファクタリングが必要になる可能性があります。これ以外の方法としては、別のコレクションライブラリを使うか、自分の手でライブラリを書くか、標準のライブラリだけで済ますか、そのいずれかの道が考えられますが、長期的に見ればどれも同様の問題を抱えることになります。私自身について言うなら、確かにGoogle Collections Libraryによって多くの時間を節約でき、コードも改善されたので、今後も可能なら仕事のみならず個人のプロジェクトでも使っていくつもりです。 本稿をGoogle Collections Libraryチームのメンバにレビューしてもらっているとき、新バージョンが準備中であることを知りました。次期バージョンのリリース日がまだ確定していないので、とりあえず本稿を先に出すことにしましたが、次のバージョンが手に入ったら、この場で変更点を再び取り上げるつもりです。 謝辞本稿を執筆するに当たって、心に決めたことがあります。1つは偉大なライブラリの存在を広く伝えることです。もう1つは入門的な記事を書くことに努め、それを超えて自分の功績を主張しないようにすることです。そのようなわけで、まずは、この数年にわたって本ライブラリの開発と改良に取り組んできたGoogle社の偉大な技術者たちに謝意を表したいと思います。最初、ソースコードに書かれている名前を抜き出そうとしたのですが、よく考えてみると、それではライブラリの改良に尽力した原作者以外の人々に感謝することになりません。そこで、Google社の多くの技術者が本ライブラリの開発と改良に携わったことを述べるにとどめました。Kevin BourrillionとJared Levyには特に感謝します。彼らは本ライブラリをオープンソース化するための準備に尽力してくれました。また、実際にオープンソースとなるまでの経緯を見守り、今後も引き続き改良することを約束してくれました。 著者紹介Dick Wall(Dick Wall)
Google社(カリフォルニア州マウンテンビュー)の技術者で、Javaテクノロジーの提唱者。Google Developer Programに所属。Java Posseポッドキャスト(http://javaposse.com)の共同司会者も務める。Java PosseではJava関連のニュースやインタビューを定期的に放送している。
関連記事 最新トップニュース
|
|