ゴールドマン・サックス発のJavaコレクションフレームワーク、その7つの特徴と歴史とは:コレクション処理の万能道具箱Eclipse Collections入門(1)(3/3 ページ)
本連載では、ゴールドマン・サックス発のオープンソースJavaコレクションフレームワークであるEclipse Collectionsについて、その概要と歴史、機能を中心に紹介します。これまでのJavaやJava 8のStream APIと比較して何が違うのか。Eclipse Collectionsを例に、読者の皆さんがコレクション処理をより深く理解するための一助になればと思います。
【4】プリミティブ型コレクション
次にプリミティブ型のコレクションを紹介します。
Eclipse Collectionsでは8種類全てのプリミティブ型(boolean、byte、short 、char、int、long、float、double)に対してList、Set、Map、Stack、Bagのコレクションが存在します。
ImmutableIntList intList = IntLists.immutable.of(1, 2, 3); ImmutableLongList longList = LongLists.immutable.of(1L, 2L, 3L); ImmutableCharList charList = CharLists.immutable.of('a', 'b', 'c'); ImmutableShortList shortList = ShortLists.immutable.of((short)1, (short)2, (short)3); ImmutableByteList byteList = ByteLists.immutable.of((byte)1, (byte)2, (byte)3); ImmutableBooleanList booleanList = BooleanLists.immutable.of(true, false); ImmutableFloatList floatList = FloatLists.immutable.of(1.0f, 2.0f, 3.0f); ImmutableDoubleList doubleList = DoubleLists.immutable.of(1.0, 2.0, 3.0);
Stream APIにもプリミティブ用のIntStream、LongStream、DoubleStreamの3種類が用意されており、「ストリームとして」プリミティブ型を扱う際に重宝します。しかし、プリミティブStreamを使っていても、メモリ上に保持する目的でコレクションに変換する際には結局ボクシングする必要があります。現在、デフォルトのJDKには、プリミティブ型の要素をメモリ上に保持できるデータ構造は配列以外に存在しません(この状況は「Valhalla」というOpenJDKのプロジェクトで、JDK 10において改善される予定です)。
簡単な例を見てみましょう。
List<Integer> employeeIds = IntStream.rangeClosed(10000, 20000).boxed().collect(Collectors.toList());
上記のように、最終的にメモリ上に保持されるコレクション内の要素はList<Integer>という形でInteger型にボクシングする必要があります。もしボクシングを避けたい場合は配列に変換するしかありません。
int[] employeeIds = IntStream.rangeClosed(10000, 20000).toArray();
一方Eclipse Collectionsは独自のプリミティブ型コレクション実装があり、プリミティブ型のコレクションをメモリ上で扱えます。
IntList employeeIds = IntInterval.fromTo(10000, 20000);
ここで、IntListはEclipse Collectionsのプリミティブリストであり、メモリ上ではIntegerではなくint型の要素を保持しています。プリミティブコレクションはボクシングした場合と比べて平均約6分の1のメモリ使用量になります。
このようにメモリ上にプリミティブ型の情報を保持しておきたいユースケースにおいてはEclipse Collectionsを使うと、ボクシングに関わるオーバーヘッドがないため、メモリ消費量と速度の両面で利点があります。さらに、プリミティブコレクション上の豊富なイテレーション処理を利用できるため、配列と比べてもずっと開発効率がよくなるでしょう。
【5】より良いパフォーマンス
Eclipse Collectionsでは、メモリ使用量や速度の最適化を目指した実装がされており、さまざまなユースケースでメモリ使用量削減やパフォーマンス向上の恩恵を受けることができます。パフォーマンスに関しては開発環境や使用条件によっても変化するので、ぜひ皆さんの環境やユースケースでベンチマークをとって比較してみてください。
- Eclipse CollectionsのUnifiedSetはHashSetと比較して平均して3分の1のメモリ使用量
- Eclipse CollectionsのUnifiedMapはHashMapと比較して平均して半分のメモリ使用量
- Eclipse Collectionsのプリミティブ型コレクションはボクシングされたコレクションと比較して平均して6分の1のメモリ使用量
- Eclipse CollectionsのFastListはArrayListと比較してequals()、hashCode()、addAll()、removeAll()、sort()などのさまざまな処理でパフォーマンス向上
- Eclipse CollectionsのasParallel()はJava 8のparallelStream()と比較してcountやfilter/mapを組み合わせたパラレル処理で20〜80%のパフォーマンス向上
【6】再利用が可能
Stream APIで得られるStreamはあくまで「流れゆくもの」であるため、メモリ上に情報を保持する目的では扱いづらい部分があります。例えば下記のように1度終端操作を行ったStreamは再利用できません。
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5); Stream<Integer> filteredIntegers = integers.stream().filter(i -> i > 3); filteredIntegers.forEach(System.out::println); filteredIntegers.map(i -> "Number" + i).forEach(System.out::println); //IllegalStateException!
上記のコードでは最後の行で下のような例外が投げられてしまいます。
java.lang.IllegalStateException: stream has already been operated upon or closed
一方Eclipse Collectionsは、あくまで「コレクション」として情報を保持する目的で実装されているので、いくらでも再利用が可能です。
ImmutableList<Integer> integers = Lists.immutable.of(1, 2, 3, 4, 5); LazyIterable<Integer> selectedIntegers = integers.asLazy().select(i -> i > 3); selectedIntegers.each(System.out::println); selectedIntegers.collect(i -> "Number" + i).each(System.out::println);
ここではasLazy()というメソッドを呼んでいますが、これはStreamのように遅延実行されるLazyIterableを返します。このLazyIterableは中間処理の状態(この場合は「select(i -> i > 3)」がメソッドチェインに追加された状態)を保持しており、上記のように何度でも再利用して終端処理を呼び出すことが可能です。
【7】チェック例外処理を簡潔に記述可能
Stream APIと組み合わせてラムダ式やメソッド参照を扱う際に、チェック例外の処理が入り込むことによって煩雑になってしまうことがあります。try-catch文が入り込むことで、本来であればラムダ式やメソッド参照によって得られるはずだった簡潔さが失われてしまうので、ちょっと切なくなります。
Appendable appendable = new StringBuilder(); List<String> list = Arrays.asList("This", " is", " a", " pen."); list.forEach(each -> { try { appendable.append(each); } catch (IOException e) { throw new RuntimeException(e); } }); System.out.println(appendable.toString()); //This is a pen.
上記の例では、appendable.append(each)がチェック例外であるIOExceptionを投げるために、ラムダ式内に例外処理を記述しています。例外処理が必要なければラムダ式ではなくメソッド参照を用いて「appendable::apend」と記述できるケースなので、ますます残念です。
一方Eclipse Collectionsでは、Procedures/Functions/Predicatesという関数型インタフェース用のユーティリティークラスにそれぞれ存在するthrowing()メソッドを使うと例外処理を簡潔に記述できます。同じ例をEclipse Collectionsで見てみましょう。
Appendable appendable = new StringBuilder(); ImmutableList<String> list = Lists.immutable.with("This", " is", " a", " pen."); list.each(Procedures.throwing(appendable::append)); System.out.println(appendable.toString()); //This is a pen.
非常にすっきりしましたね。上記の例では「Procedures.throwing()」というメソッドにチェック例外を投げるメソッド参照appendable::appendを渡してやることで、例外処理を内部処理してくれます。このユーティリティーは例外をRuntimeExceptionにラップして投げるだけという単純な例外処理の場合のみ有効ですが、この簡潔さはやみつきになると思います。
Eclipse Collectionsに至るまでの歴史
連載初回の最後にEclipse Collectionsの歴史を振り返ります。
Eclipse Collectionsの祖先となるフレームワークは、「Caramel」という名で2004年にゴールドマン・サックス社内で産声を上げました。もともとSmalltalkプログラマーであったDonald Raab氏が、SmalltalkのイテレーションプロトコルをJavaにも導入したいと考えたのが開発のきっかけです。
Rubyを使ったことがある方は、Eclipse Collectionsのselect()やcollect()など、RichIterableのAPI名がRubyのEnumerableのAPI名と似ていることに気付くかもしれません。これは同じSmalltalk由来の命名だからなんですね(Rubyには、Stream APIでも使われている、Lisp由来のfilter()やmap()などのメソッド名も存在します)。その後 Caramelはゴールドマン・サックス社内で広く使われるようになり、2012年、Java 8のリリースに先駆け、「GS Collections」と呼ばれるオープンソースJavaコレクションフレームワークとしてGitHubに公開されました。
GS Collectionsは2014年のQCon NYやJavaOneのセッションが話題になり、人気が上昇してきました。日本でも2015年のJava Day TokyoやJJUG CCCでの発表により認知が広まりました。
過去10余年で約40人の開発者が携わってきたこのプロジェクトは、GitHub上ではApache 2.0 Licenseで公開され、実際の開発・管理はゴールドマン・サックス社内のSubversionリポジトリで行われてきました。リリースのタイミングのみに社内SubversionからGitHubへと公開されるワークフローをとっていたため、オープンソースではあるものの知的財産管理の側面から外部のコントリビューションを受け付けるのが難しい状況でした。
より多くのユーザーに使ってもらい、より良いフレームワークへと育てていくためには、よりオープンな開発を行うべきであるとの考えから、2015年10月にゴールドマン・サックスはEclipse Foundationへの参加を表明しました。さらに、その翌日にサンフランシスコで開催されたJavaOne 2015で、GS CollectionsをEclipse Collectionsという名前にし、Eclipse Foundationへ移管することが発表されました。この一連の動きにより、2015年12月にはGitHubのソースコード移行が完了し、2016年1月にEclipse Collections 7.0が正式リリースとなりました。
筆者は創始者のDonald Raab氏と共にコミッター兼共同プロジェクトリードを務めており、日本と米国のコミッターを中心に開発を進めています。Eclipse CollectionsはOSSとしてコミュニティーからのコントリビューションを広く受け付けており、実は本記事の執筆中に日本からのコントリビューター第1号が誕生しました。
プログラマーは「怠惰であることが美徳」
以上、連載第1回としてEclipse Collectionsの概要と歴史を駆け足で紹介してまいりましたが、開発者にとっては「やりたいことをやりたいように簡潔に記述できる」点が一番大きいのではないでしょうか。コレクションの処理といえばプログラマーにとって日常使わない日はないといっても過言ではないくらいありふれた作業です。
われわれプログラマーは「怠惰であることが美徳」とよく言われますが、コレクションに関わる処理を何度も自分で記述したり冗長に書いたりするよりは、本来実装すべき機能のコーディングに集中したいものです。Eclipse Collectionsは使えば使うほど開発者の手になじむ、楽しい開発を行うための道具箱になるかと思います。
次回は、Eclipse Collectionsの初期化方法と基本的な機能を紹介しますのでお楽しみに。
筆者紹介
伊藤博志
Eclipse Collectionsの共同プロジェクトリード兼コミッター。ゴールドマン・サックス テクノロジー部 Enterprise Platforms アジアパシフィックチームに所属するエンジニア。2005年ゴールドマン・サックス入社。PARAアジアパシフィックチームのテクニカルアーキテクトを経て、現在ヴァイス・プレジデント。
GitHub:https://github.com/itohro/
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 初心者のためのJavaラムダ式入門とJDKのインストール、IDEの環境構築
本連載では、今までJavaの経験はあっても「ラムダ式は、まだ知らない」という人を対象にラムダ式について解説していきます。初回は、ラムダ式の概要と利点、必要性、JDK 8のセットアップ、NetBeans、IntelliJ IDEA、Eclipseの環境構築について。 - キュー構造をJavaで実装してジェネリック型を理解する
これからプログラミングを学習したい方、Javaは難しそうでとっつきづらいという方のためのJavaプログラミング超入門連載です。最新のEclipse 3.4とJava 6を使い大幅に情報量を増やした、連載「Eclipseではじめるプログラミング」の改訂版となります - コレクションフレームワークを拡張するCollections