はじめに
Eclipse Collectionsは、ゴールドマン・サックス社製のJavaのコレクションフレームワークで、現在はEclipse Foundationに移管されオープンソースで開発が進められています。Stream APIよりも簡潔な記述ができてしかも高機能、ImmutableListなどJavaの標準言語仕様には無い豊富なコレクション型が使用できるのが特徴のようです。
以前から名前だけは知っていたのですが、今回こちらの記事に触発されて、ちょっと試してみました。なお、Eclipse Collectionsを試すにあたり、Java7までの書き方やStream APIとも併せて比較してみたいと思います。
環境
- Java 8
- Eclipse Collections 8.0.0-M1
- lombok 1.16.10
- Gradle 2.14.1
- IntelliJ IDEA
準備
build.gradleにEclipse Collectionsの定義を追加します。それと可読性のためにlombokも。
compile group: 'org.eclipse.collections', name: 'eclipse-collections', version: '8.0.0-M1'
compile group: 'org.projectlombok', name: 'lombok', version: '1.16.10'
lombokに関しては、今回はIntelliJ IDEAを使用するので、lombokプラグインを別途インストールする必要があります。
それと、データの格納用クラスとして、Personクラスを定義しておきます。@lombok.Value
アノテーションを付けているので、全フィールドを引数としたコンストラクタとゲッターメソッドのみを持つイミュータブルなクラスになります。
@lombok.Value
class Person {
private String name;
private int age;
}
抽出処理
まずはコレクションの抽出処理です。
やりたいこととしては、以下のようなPersonのリストがあるとして、この中から30歳以上の要素だけを抽出してみます。
List<Person> persons = Arrays.asList(
new Person("aaa", 10),
new Person("bbb", 20),
new Person("ccc", 30),
new Person("ddd", 40),
new Person("eee", 50)
);
Java7までの書き方
Java7まではループを使って抽出してましたよね。
List<Person> results = new ArrayList<>();
for (Person p : persons) {
if (p.getAge() >= 30) {
results.add(p);
}
}
Stream API
Stream APIだとこんな感じにワンライナーで書けますよね。
ただ、stream
とかcollect
とか冗長な書き方だなと思ってました。
List<Person> results = persons.stream()
.filter(p -> p.getAge() >= 30)
.collect(Collectors.toList());
Eclipse Collections
Eclipse Collectionsを使って同じことをやる場合、Eclipse Collectionsで用意されているコレクション型を使用した方が良さそうなので、org.eclipse.collections.api.list.ImmutableList
でイミュータブルなPersonリストを別途定義します。
ImmutableList<Person> persons2 = Lists.immutable.of(
new Person("aaa", 10),
new Person("bbb", 20),
new Person("ccc", 30),
new Person("ddd", 40),
new Person("eee", 50)
);
それを踏まえて、抽出処理はこんな感じで書きます。
ImmutableList<Person> results = persons2.select(p -> p.getAge() >= 30);
無駄なものが無くなってスッキリしましたね。
もちろん、最初にjava.util.List
で定義したPersonリストに対してもこう書けば抽出はできます。
MutableList<Person> results = ListIterate.select(persons, p -> p.getAge() >= 30)
この場合の返却値はorg.eclipse.collections.api.list.MutableList
という、Eclipse Collectionsのミュータブルなコレクション型で返却されます。
ちなみに、java.util.List
で定義したPersonリストをorg.eclipse.collections.api.list.ImmutableList
に変換する場合はこんな感じで書くみたいです。
ImmutableList<Person> persons2 = Lists.immutable.withAll(persons);
ソート処理
次はコレクションのソートです。
前項で定義しておいたPersonクラスのリストを年齢の降順でソートしてみます。
Java7までの書き方
Java7まではComparatorを使って書いてましたよね。
p2.getAge() - p1.getAge()
っていう書き方には慣れが必要でした。
また、引数で与えたリストに対して破壊的変更を加えてしまうこともいただけない。
Collections.sort(persons, new Comparator<Person>() {
@Override
public int compare(Person p1, Person p2) {
return p2.getAge() - p1.getAge();
}
});
Stream API
Stream APIでは、Comparator.comparing()とか使えば可読性が少し上がりますね。
List<Person> results = persons.stream()
.sorted(Comparator.comparing(Person::getAge).reversed())
.collect(Collectors.toList());
Eclipse Collections
Eclipse Collectionsの場合は、先ほどのImmutableListではソートが使えないみたいなので、MutableListで改めて定義します。
MutableList<Person> persons3 = Lists.mutable.of(
new Person("aaa", 10),
new Person("bbb", 20),
new Person("ccc", 30),
new Person("ddd", 40),
new Person("eee", 50)
);
で、ソートはこんな感じ。
MutableList<Person> results =
persons3.sortThis(Comparator.comparing(Person::getAge).reversed());
sortThis
の引数はStream APIと同じ書き方をしています。
コレクションの編集処理
Personクラスのリストに対して各要素に以下の編集をしてみます。
-
name
の値の末尾に"さん"
を付加 -
age
の値の末尾に"歳"
を付加 - 上記を以下のクラスに格納する
@lombok.Value
class OtherBean {
private String name;
private String age;
}
Java7までの書き方
Java7だったらループの中で編集をして結果リストに一つ一つ追加してました。
List<OtherBean> results = new ArrayList<>();
for (Person p : persons) {
String name = p.getName() + "さん";
String age = p.getAge() + "歳";
results.add(new OtherBean(name, age));
}
Stream API
Stream APIの場合は、mapメソッドに編集処理とOtherBeanクラスの生成処理を書きます。
List<OtherBean> results = persons.stream()
.map(p -> {
String name = p.getName() + "さん";
String age = p.getAge() + "歳";
return new OtherBean(name, age);
})
.collect(Collectors.toList());
Eclipse Collections
Eclipse Collectionsでは、collectだけで書けちゃいます。
ImmutableList<OtherBean> result = persons2.collect(p -> {
String name = p.getName() + "さん";
String age = p.getAge() + "歳";
return new OtherBean(name, age);
});
集計処理
次はPersonリストの平均年齢を求めてみます。
Java7までの書き方
Java7だったらループで年齢の合計値を求めたうえでリストのサイズで割って平均を求める感じ。
int sum = 0;
for (Person p : persons) {
sum += p.getAge();
}
double average = (double) sum / list.size();
Stream API
Stream APIの場合は、average()というメソッドがあるので簡単です。
double average = persons.stream().mapToInt(Person::getAge).average().getAsDouble();
Eclipse Collections
Eclipse Collectionsの場合は更に簡単に書けます。
double average = persons2.collectInt(Person::getAge).average();
まとめ
Eclipse Collectionsは、Stream APIよりも簡潔に書くことができて好印象です。ただ、今回試したような単純な処理ではStream APIとのパフォーマンスの差はあまり感じられませんでした。今後、色々なケースで比較をしていきたいと思います。また、今回はEclipse Collectionsのほんのさわり部分しか試していないので、今後は例外ハンドリングとか今回登場していないコレクション型なども試してみたいと思います。
おまけ
試していてよく分からなかったのが、無限リストの扱いってどうやるのかなと。
例えば、ある日を基準としてn日間分の以下のような日付のマップデータを作成する場合を考えてみます。
{
20160808=2016/08/08 (月),
20160809=2016/08/09 (火),
20160810=2016/08/10 (水),
20160810=2016/08/11 (木),
20160810=2016/08/12 (金),
}
これはプルダウンに設定するマップを想定していますが、これをStream APIを使って書く場合はこんな感じになると思います。
DateTimeFormatter kfmt = DateTimeFormatter.ofPattern("yyyyMMdd");
DateTimeFormatter vfmt = DateTimeFormatter.ofPattern("yyyy/MM/dd (E)");
Map<String, String> map =
Stream.iterate(LocalDate.of(2016, 8, 8), d -> d.plusDays(1))
.limit(10)
.collect(Collectors.toMap(
kfmt::format,
vfmt::format,
(k, v) -> v,
LinkedHashMap::new));
この例では、Stream.iterate(...)
で2016/8/8を基準に1日ずつ増やしていく無限リストを定義して、そこからlimit(...)
で10日分を抽出し、collect(...)
でキーは"yyyyMMdd"
、値は"yyyy/MM/dd (E)"
でフォーマットして、LinkedHashMapに格納する、のですが。。。
こういう処理みたいなことを、Eclipse Collectionsで書く場合ってどうやったら良いのかなと。
Version 8.0.0からはJava8との親和性が強化されるって書いてあったので、Stream APIと組み合わせれば良いのかな?
今回はここまで。