不変コレクション
Java Collections Frameworkが開発された当時、変更不能なコレクションを返す仕組みが導入されましたが、これはコレクションの内容が誤って変更されないようにコレクションにラッパーをかぶせるというものでした。
しかし、この変更不能コレクションには欠点があります。実は、変更できてしまうのです。変更不能コレクションを受け取った側が内容を変更できなくても、変更不能コレクションのベースとなるオリジナルのコレクションはまだ変更でき、両者がベースのコレクションを共有しているので、そのことが変更不能コレクションに影響します。
つまり、プログラマが変更不能コレクションを変更できないと決めてかかっても、それは正しくありません。
Google Collections Libraryでは、これに代わるものとして不変(immutable)コレクションが導入されています。オリジナルのコレクションから複製したコレクションを変更できないようにするので、これを受け取った側はコレクションが決して変更されないことを信頼することができます。
次に例を示します。
final List<String> immutableList = Lists.immutableList("Hello", "World"); // UnsupportedOperationException! immutableList.add("!");
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);
final参照と不変コレクションを組み合わせるやり方がよく用いられますが、それでも安心してはいけません。コレクション自体を変更できなくても(つまり、要素の追加、削除、置換ができなくても)、コレクション内の可変オブジェクトはまだ変更できるからです。そのため、不変コレクションに可変値を格納しないようにすることをお勧めします。無用な混乱を招くだけでなく、不変コレクションを使うことの意義が失われるからです。無論、例外は必ずありますが、不変と可変を混在させるときは、ドキュメントにその理由と経緯を明記しておくべきです。
関数型プログラミング的な機能
ここまで読まれた方は、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));
この例では、expensiveCarというプレディケートを作成しています。これは自動車の価格が50000を超えるとtrueを返します。プレディケートのapplyメソッドには選択基準を記述する必要があります。これを自動車のリストにIterables.filter()またはIterators.filter()で適用すると、premiumCarsの不変リスト(TeslaとMcLaren F1のみを含む)が作られます。
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); }
new Functionのジェネリック(総称的)な型シグニチャは、この関数がIntegerを受け取り、それを何らかの方法でStringに変換することを示唆しています。applyメソッドは、これらのジェネリックな型に従い、Integer型のfrom値を受け取ってString型の値を返します。
この例では、Integerを対応するString名にマップするために、以前作成した双方向マップ(BiMap)のnumbersを使用しています。その後、Iterables.transform関数でIntegerのArrayListを変換して対応する名前を求めます。簡単で、しかもエレガントなやり方です。
この例から関数の効用はよくわかりますが、実際には同じことがもっと簡単にできます。Functionsクラスに静的な補助メソッドがいくつか定義されており、よく使う関数はそれらのメソッドで提供されます。たとえば、そのうちの1つであるforMap()は、マップを受け取り、そのマップをコレクションに適用する関数を作ります。したがって、これを上の例に当てはめると、Function内部クラスの定義を次のように書き換えることができます。
Function<Integer, String> nameForNumber = Functions.forMap(numbers);
同様のクラスにPredicatesとConstraintsがあり、よく使うプレディケートや制約があらかじめ定義されています。
制約(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には特に感謝します。彼らは本ライブラリをオープンソース化するための準備に尽力してくれました。また、実際にオープンソースとなるまでの経緯を見守り、今後も引き続き改良することを約束してくれました。
