BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル 実例で学ぶGS Collections – Part 1

実例で学ぶGS Collections – Part 1

原文(投稿日:2014/09/22)へのリンク

私はゴールドマン・サックスでJavaエンジニアとして働いています。テクノロジー・フェローであり、マネージング・ディレクターでもあります。ゴールドマン・サックスが2012年の1月にオープンソースとして公開した GS Collections フレームワークの創作者で、かつてはSmalltalkのエンジニアとして働いていました。

Javaを仕事に使うようになった頃、私はSmalltalkにあった2つの機能を切望していました。

  1. Smalltalkのブロック構文(ラムダ)
  2. Smalltalkの持つ素晴らしく機能が豊富なコレクションフレームワーク

当時は、この2つの機能がJavaのコレクションインターフェースと互換性を持つ形で実装されることを望んでいたのです。しかし2004年頃には、私がJavaに欲しいと思う機能を実装するような人はどこにもいないと確信しました。さらにこの頃には、自分が今後少なくとも10年~15年はJavaでプログラミングをするキャリアを積んでいくと考えていたので、自分が欲しいと思うものは自分で作ることに決めたのです。

それから早10年が過ぎ、今では私がJavaに望んでいたもの、ほぼすべてが利用できるようになりました。Java 8でラムダのサポートが提供されたおかげで、ラムダとメソッド参照の恩恵を受けることができるのです。Javaのコレクションフレームワークのなかでもっとも機能が豊富であるといえるであろうGS Collectionsとともに。

下記にGS Collections、Java 8、Guava、Trove、Scalaが提供する機能の比較表を示します。ここに挙げてあるのは皆さんがコレクションフレームワークに期待する機能のすべてではないかもしれませんが、過去10年に私や他のゴールドマン・サックスのエンジニア達がJavaで必要とした機能が挙げられています。

Features

GSC 5.0

Java 8

Guava

Trove

Scala

Rich API

 

Interfaces

Readable, Mutable, Immutable, FixedSize, Lazy

Mutable, Stream

Mutable, Fluent

Mutable

Readable, Mutable, Immutable, Lazy

Optimized Set & Map

 (+Bag)

   

 

Immutable Collections

 

 

Primitive Collections

(+Bag, +Immutable)

   

 

Multimaps

 (+Bag, +SortedBag)

 

 (+Linked)

 

(Multimap trait)

Bags (Multisets)

 

   

BiMaps

 

   

Iteration Styles

Eager/Lazy,
Serial/Parallel
Lazy,
Serial/Parallel
Lazy,
Serial
Eager,
Serial

Eager/Lazy, Serial/Parallel (Lazy Only)

昨年のjClarityのインタビューで、GS Collectionsを便利に使ういくつかの機能の組み合わせを紹介しました。元記事はこちらから読むことができます。

Java 8がリリースされ、Stream APIが提供されている今となっては、なぜGS Collectionsを使う必要があるのか疑問に思うかもしれません。Stream APIはJavaコレクションフレームワークにとって大きな前進ではありますが、必要な機能がすべて実装されているわけではありません。上の表にもあるように、GS CollectionsではMultimapやBag、イミュータブルなコンテナ、プリミティブ型専用のコンテナなどが提供されています。HashSetやHashMapをより最適化した代替実装や、それらの利点を生かしたBagやMultimapの実装などもあります。GS Collectionsのイテレーションパターンはコレクションインターフェース上に存在しているので、stream()のようにAPI内に「入る」処理を呼んだりcollect()のようにAPIから「出る」処理を呼ぶ必要がありません。おかげで、多くの場面でより簡潔なコードが書けるのです。さらに、GS CollectionsはJava 5までの後方互換性を保っています。これは、ライブラリの開発者にとっては特に重要なポイントになります。Javaのメジャーバージョンがいくつかリリースされた後でも、たいていは古いバージョンのJavaをサポートしなければならないからです。

ここからは例を挙げながら、これらの機能の活用法の数々をお見せしたいと思います。以下の例は、GS Collections Kataと呼ばれる研修教材を基に作成したものです。GS Collections Kataは実際にゴールドマン・サックスのエンジニアにGS Collectionsの使い方を教える教材として使用されており、GitHub内のリポジトリに公開されています。

例1: コレクションのフィルタ

GS Collectionsの最も典型的な使用法は、コレクションをフィルタすることでしょう。GS Collectionsでは様々な組み合わせの実装方法を提供しています。

先に挙げた研修教材のGS Collections Kataでは、顧客(customers)のリストをフィルタすることから始めることが多いです。例えば、顧客のリストの中からロンドン在住の顧客のみを含むリストを抽出したいとします。下記のコードではselectパターンと呼ばれるイテレーションパターンを用いて実装しています。

import com.gs.collections.api.list.MutableList; 
import com.gs.collections.impl.test.Verify; 

@Test public void getLondonCustomers() { 
      MutableList<Customer> customers = this.company.getCustomers(); 
      MutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
      Verify.assertSize("Should be 2 London customers", 2, londonCustomers); 
} 

MutableList内のselectメソッドはMutableListを返します。このコードは先行評価されるので、select()メソッドの呼び出しが終了するまでにすべての処理(すなわち、元のリストから条件に適合する要素が取り出されターゲットとなるリストに追加されるまで)が実行されます。この“select” という名称はSmalltalkの同処理から受け継がれています。Smalltalkにはselect(別名filter)、reject(別名filterNot)、collect(別名maptransform)、detect(別名findOne)、detectIfNoneinjectInto(別名foldLeft)、anySatisfyallSatisfyと名付けられた基本的なコレクションプロトコルが提供されています。

同様の処理を遅延評価を用いて実装したい場合は、下記のように書けます。

MutableList<Customer> customers = this.company.getCustomers(); 
LazyIterableCustomer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

asLazy()というメソッドを呼ぶだけで、他のコードはほぼそのままです。asLazy()を呼ぶとselectの返り値の型が変わり、MutableListの代わりにLazyIterableが返ってきます。これは、Java 8のStream APIを用いた下記のコードとまったく同じ処理になります。


List<Customer> customers = this.company.getCustomers(); 
Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); 
List londonCustomers = stream.collect(Collectors.toList()); 
Verify.assertSize(2, londonCustomers); 

上記では stream() メソッドを呼んだ後にfilter()メソッドがStreamを返します。サイズを評価するためには、上記のように一旦StreamからListに変換するか、下記のようにJava 8のStream.count()メソッドを使う必要があります。


List<Customer> customers = this.company.getCustomers(); 
Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); 
Assert.assertEquals(2, stream.count()); 

GS CollectionsのMutableListLazyIterableは両方ともRichIterableというインターフェースを継承しています。実際、これまでのコードはすべてRichIterableのみを使って書き換えることができます。RichIterableのみを使った例として、まずは遅延評価を使って下記のように書けます。


RichIterable<Customer> customers = this.company.getCustomers(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

さらに、先行評価を使って下記のように書けます。


RichIterable<Customer> customers = this.company.getCustomers(); 
RichIterable<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
Verify.assertIterableSize(2, londonCustomers); 

上記のように、RichIterableLazyIterableMutableListの共通インターフェースなので、これらを置き換えて使うことができます。

顧客のリストをイミュータブルにすることも可能です。ImmutableListを使うと、下記のように型が置き換わります。


ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); 
ImmutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London"));
Verify.assertIterableSize(2, londonCustomers); 

ほかのRichIterableの実装と同様に、ImmutableListを遅延評価を使ってイテレートすることも可能です。


ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

MutableList ImmutableListの共通インターフェースにはListIterableと呼ばれるものがあり、これらの型を置き換えて、より汎用的な型として使うことができます。RichIterableListIterableの親にあたります。上記のコードをより汎用的に書くと以下のようになります。


ListIterable<Customer> customers = this.company.getCustomers().toImmutable(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

さらに汎用的に書くならば、以下のようになります。


RichIterable<Customer> customers = this.company.getCustomers().toImmutable(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

GS Collectionsのインターフェース階層は、非常に基本的なパターンに沿っています。それぞれの型(List、Set、Bag、Map)に対して、読み出し可能なインターフェース(ListIterableSetIterableBagMapIterable)、ミュータブルなインターフェース(MutableListMutableSetMutableBagMutableMap)、そしてイミュータブルなインターフェース(ImmutableList, ImmutableSet, ImmutableBag, ImmutableMap)があります。

(図をクリックして拡大)

図1.GSCコンテナの基本的なインターフェース階層

下記は、同じコードをListの代わりにSetを使用して書き換えた例です。


MutableSet<Customer> customers = this.company.getCustomers().toSet(); 
MutableSet<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

下記は、同様にSetで遅延評価を使って書いた解法です。


MutableSet<Customer> customers = this.company.getCustomers().toSet(); 
LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London"));
Assert.assertEquals(2, londonCustomers.size()); 

下記は、Setを使った解法を、最も汎用的なインターフェースを使用して書いたコードです。


RichIterable<Customer> customers = this.company.getCustomers().toSet(); 
RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

次に、あるコンテナ型から別のコンテナ型へと変換するのに使える便利な機能を紹介しましょう。まずは、遅延評価を使ってフィルタするとともに、ListからSetへと変換してみましょう。


MutableList<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> lazyIterable = customers.asLazy().select(c -> c.livesIn("London")); 
MutableSet<Customer> londonCustomers = lazyIterable.toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

これらは流暢なインターフェースとして設計されているので、メソッドチェインでまとめて書くことが可能です。


MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .asLazy() 
       .select(c -> c.livesIn("London")) 
       .toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

この書き方がコードの可読性に影響するかどうかの判断は読者におまかせします。私は、コードを読む人の理解の助けになると感じたら、たいていメソッドチェインの途中で分割して途中経過の型を見せるようにしています。読むコードの量は多くなりますが、頻繁にコードを読まない人にとってはこの方が理解しやすいからです。

さて、ListからSetへの変換は、selectメソッドそのもので実現することも可能です。selectメソッドにはオーバーロードメソッドが存在し、Predicateを第一引数に、結果を格納するコレクションを第二引数に渡すことができます。


MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), UnifiedSet.newSet()); 
Assert.assertEquals(2, londonCustomers.size()); 

このメソッドを使うとどんなコレクション型でも返すことができることに注目してください。下記の例ではMutableBagを返しています。


MutableBag<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), HashBag.newBag()); 
Assert.assertEquals(2, londonCustomers.size()); 

下記の例では、JDKで提供されているCopyOnWriteArrayListを返しています。ここでのポイントは、java.util.Collectionを実装するクラスであればどんな型でも返すことができるということです。


CopyOnWriteArrayList<Customer> londonCustomers = 
       this.company.getCustomers() 
       .select(c -> c.livesIn("London"), new CopyOnWriteArrayList<>()); 
Assert.assertEquals(2, londonCustomers.size()); 

これまですべての例でラムダを使ってきました。selectメソッドは、Predicateという下記のように定義されたGS Collectionsの関数型インターフェースを引数にとります。


public interface Predicate extends Serializable { 
       boolean accept(T each); 
} 

ここまで使ってきたラムダは非常にシンプルなものでした。下記に、今まで使ってきたラムダがコードとしてどう表現されるか分かり易くするために、変数として抽出してみます。


Predicate<Customer> predicate = c -> c.livesIn("London"); 
MutableList<Customer> londonCustomers = this.company.getCustomers().select(predicate); 
Assert.assertEquals(2, londonCustomers.size()); 

Customerクラスに定義されているlivesIn()メソッドは単純で、下記のように定義されています。


public boolean livesIn(String city) { 
       return city.equals(this.city); 
} 

livesInメソッドが定義されているので、ラムダを使う代わりにメソッド参照を使えたらうれしいですよね。ですが、下記のコードだとコンパイルできません。


Predicate<Customer> predicate = Customer::livesIn; 

コンパイラは以下のようなエラーを出力します。


Error:(65, 37) java: incompatible types: invalid method reference 
      incompatible types: com.gs.collections.kata.Customer cannot be converted to java.lang.String 

これは、上記のメソッド参照が、Customerとcityを表すStringの2つのパラメータを必要としているからです。この場合、Predicate の代わりにPredicate2が使えます。


Predicate2<Customer, String> predicate = Customer::livesIn;

上記ではPredicate2CustomerStringの2つのジェネリクス型を指定していることに注目してください。Predicate2を使うにはselectの派生であるselectWith メソッドを下記のように使用します。


Predicate2<Customer, String> predicate = Customer::livesIn; 
MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(predicate, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

上のコードは、下記のようにメソッド参照をインライン化することでさらに簡略化できます。


MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size());

“London”という文字列は、Predicate2に定義されるメソッドが呼ばれる度に第二引数として渡されます。第一引数には、リストからCustomerオブジェクトがそれぞれ渡されます。

selectWithメソッドは、selectメソッドと同様RichIterableに定義されています。したがって、これまでselect を使って説明してきた例はすべてselectWithでも使えます。すなわち、ミュータブル、イミュータブル等すべてのインターフェースが使えたり、様々なコレクション型を返すことができたり、遅延評価を利用したりできるのです。selectが第二引数に結果を格納するCollectionを指定できたように、selectWithでは第三引数に指定できます。

例えば、下記のコードはselectWithを使って、ListをフィルターしてSetに変換しています。


MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .selectWith(Customer::livesIn, "London", UnifiedSet.newSet());
Assert.assertEquals(2, londonCustomers.size());

これは遅延評価を使うと下記のように書くことができます。


MutableSet<Customer> londonCustomers = 
       this.company.getCustomers() 
       .asLazy() 
       .selectWith(Customer::livesIn, "London") 
       .toSet(); 
Assert.assertEquals(2, londonCustomers.size()); 

最後に、selectselectWithメソッドは、java.lang.Iterableを継承しているどんなコレクション型に対しても使えることをお見せしたいと思います。これは、JDKで提供されるコレクション型でも、サードパーティライブラリのコレクション型でも同様です。GS Collectionsに一番最初から存在するクラスの中に、Iterateと呼ばれるユーティリティクラスがあります。Iterateを使用してIterableに対してselectメソッドを呼ぶコードを書いてみましょう。


Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = Iterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

selectWithだと下記のようになります。


Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = Iterate.selectWith(customers, Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

格納先のCollectionを指定できる方法もあります。すべての基本的なイテレーションプロトコルはIterateで提供されています。さらに遅延評価を用いたユーティリティクラス(LazyIterate)も、java.lang.Iterableを継承したコンテナに対して使えます。例えば、


Iterable<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = LazyIterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

さらに、オブジェクト指向なAPIを使ってよりエレガントに書きたいならば、Adapterクラスを使って実現できます。下記では、ListAdapterjava.util.Listに対して使用しています。


List<Customer> customers = this.company.getCustomers(); 
MutableList<Customer> londonCustomers = 
       ListAdapter.adapt(customers).select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

ここまで読み進まれた読者ならばお察しのとおり、遅延評価を使った書き方もできます。


List<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = 
    ListAdapter.adapt(customers) 
    .asLazy() 
    .select(c -> c.livesIn("London"));
Assert.assertEquals(2, londonCustomers.size()); 

selectWith()も、 ListAdapterを使って遅延評価ができます。


List<Customer> customers = this.company.getCustomers(); 
LazyIterable<Customer> londonCustomers = 
        ListAdapter.adapt(customers) 
        .asLazy() 
        .selectWith(Customer::livesIn, "London"); 
Assert.assertEquals(2, londonCustomers.size()); 

同様に、SetAdapterjava.util.Setのどの実装に対しても使うことができます。

さて、もしあなたがデータレベルの並列処理によって恩恵を得られるような問題を抱えているならば、以下の2つの解法のうちどちらかが使えるでしょう。まずは、ParallelIterateクラスを使用して解決する方法をお見せします。これは先行評価/並列処理を行うアルゴリズムを用います。


Iterable<Customer> customers = this.company.getCustomers(); 
Collection<Customer> londonCustomers = ParallelIterate.select(customers, c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.size()); 

ParallelIterateクラスはIterableオブジェクトを引数にとり、常にjava.util.Collectionを返します。ParallelIterateはGS Collectionsでは2005年から提供されています。以前は先行評価/並列処理のみがサポートされていたのですが、バージョン5.0のリリースからは、遅延評価/並列処理がRichIterableのAPIとしてサポートされるようになりました。RichIterableには先行評価/並列処理は存在しません。これは、遅延評価/並列処理がデフォルトとしてふさわしいと考えたからです。遅延評価/並列処理APIの利便性に関する皆さんからのフィードバックによっては、いずれRichIterableに先行評価/並列処理APIを足すことがあるかもしれません。

もう一つの解法として、遅延評価/並列処理のAPIを用いて解くならば、下記のように書けるでしょう。


FastList<Customer> customers = this.company.getCustomers(); 
ParallelIterable<Customer> londonCustomers = 
     customers.asParallel(Executors.newFixedThreadPool(2), 100) 
        .select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.toList().size()); 

執筆時点では、asParallel()メソッドはGS Collections内のいくつかのコンテナ実装にしか存在していません。このAPIはMutableListListIterableRichIterableなどのインターフェースにはまだ導入されていません。現在asParallel()メソッドはExecutorServiceとバッチサイズの2つの引数をとります。いずれは、バッチサイズを自動的に計算するバージョンのasParallel()を提供するかもしれません。

この例では、より特定の型(この場合ParallelListIterable)を使用することも可能です。


FastList<Customer> customers = this.company.getCustomers(); 
ParallelListIterable<Customer> londonCustomers = 
     customers.asParallel(Executors.newFixedThreadPool(2), 100) 
          .select(c -> c.livesIn("London")); 
Assert.assertEquals(2, londonCustomers.toList().size()); 

ParallelIterableにはParallelListIterableParallelSetIterableParallelBagIterableを含むクラス階層があります。

本稿では、GS Collectionsのselect()メソッドとselectWith()メソッドを用いて、コレクションをフィルタするための様々な方法を紹介しました。先行評価と遅延評価、直列と並列、GS CollectionsのRichIterable階層にあるさまざまな型など、無数の組み合わせが存在することをお見せできたかと思います。

来月に執筆する予定のPart 2では、collect、groupBy、flatCollect、いくつかのプリミティブ型のコンテナと、それらに付随する豊富なAPIをご紹介したいと思います。Part 2でご紹介する例では細かい枝葉にわたった詳細や、様々なオプションを網羅することはしませんが、本稿で紹介したような様々な機能の組み合わせ方法やオプションが存在すると考えて差し支えないことをお伝えしておきます。

著者について

Donald Raab ゴールドマン・サックス テクノロジー部Enterprise PlatformsグループにてJVMアーキテクチャチームを統括。JSR 335 エキスパートグループ(Lambda Expressions for the Java Programming Language)の一員であり、JCP Executive Committeeのゴールドマン・サックス共同代表を務める。2001年PARAチームのテクニカルアーキテクトとしてゴールドマン・サックスに入社。2007年テクノロジー・フェロー、2013年マネージング・ディレクター就任。

翻訳者について

伊藤博志 ゴールドマン・サックス テクノロジー部Enterprise PlatformsグループのJVMアーキテクチャチームに所属するJavaエンジニア。2005年ゴールドマン・サックス入社。PARAアジアパシフィックチームのテクニカルアーキテクトを経て、現在ヴァイス・プレジデント。

GS Collectionsやゴールドマン・サックスのエンジニアリングに関する詳しい情報は、 www.gs.com/engineeringをご参照下さい。

© 2014 Goldman, Sachs & Co.

本掲載記事は、Goldman, Sachs & Coのテクノロジー部において入手した情報をもとに作成されたもので、情報の提供を目的としております。本記事によるなんらかの行動を勧誘または推奨するものではありません。本記事における意見は明示されていない限りGoldman, Sachs & Coの意見と異なるかもしれません。本記事は信頼できると思われる情報に基づいて作成されていますが、当社はその正確性、完全性に関する責任を負いません。 ご利用に際しては、ご自身の判断にてお願いします。本記事資料の一部又は全部を本ディスクレーマーなしに第三者に転送することを禁じます。

この記事に星をつける

おすすめ度
スタイル

BT