2015/06/13、関ジャバ主催の GS Collections 道場[ハンズオン][OSS コレクションフレームワーク] - connpass に行ってきたときに、新たに学んだこととかのメモ。
Predicates
Predicate
(boolean
を返す関数インターフェース)を生成するためのスタティックメソッドがたくさん定義されたクラス。
import java.util.List;
import com.gs.collections.impl.block.factory.Predicates;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
List<Integer> list = Lists.mutable
.of(1, 5, 10, 15)
.select(Predicates.lessThan(10));
System.out.println(list);
}
}
[1, 5]
Java 8 より前の環境で、ラムダ式が使えないときに便利そう。
Javadoc を眺めると、便利そうなメソッドがちらほらと。。。
import java.util.List;
import static com.gs.collections.impl.block.factory.Predicates.*;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
List<String> list = Lists.mutable
.of("one", "two", "three", "four")
.select(in("two", "four"));
System.out.println(list);
}
}
[two, four]
static インポートすれば、さらにコードが簡潔になって綺麗な感じ。
あと、メソッド参照と組み合わせて使うタイプのものもある。
import java.util.List;
import static com.gs.collections.impl.block.factory.Predicates.*;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
List<String> list = Lists.mutable
.of("one", "two", "three", "four")
.select(attributeLessThan(String::length, 5));
//.select(str -> str.length() < 5); と同じ
System.out.println(list);
}
}
[one, two, four]
この場合は、ラムダ式の方が良いと思う人もいるかも知れない(自分とか)。
Java 7 以下の環境でメソッド参照もどきを作る
メソッド参照が使えない環境でも、以下のように工夫することでメソッド参照もどきが作れる。
import com.gs.collections.api.block.function.Function;
public class Person {
public static Function<Person, Integer> GET_AGE = new Function<Person, Integer>() {
@Override
public Integer valueOf(Person hoge) {
return hoge.age;
}
};
private int age;
private String name;
public Person(String name) {
this.name = name;
}
public Person age(int age) {
this.age = age;
return this;
}
@Override
public String toString() {
return this.name + "(" + this.age + ")";
}
}
import java.util.List;
import static com.gs.collections.impl.block.factory.Predicates.*;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
List<Person> list = Lists.mutable
.of(
person("Sato").age(18),
person("Suzuki").age(26),
person("Tanaka").age(24)
)
.select(attributeLessThan(Person.GET_AGE, 25));
System.out.println(list);
}
private static Person person(String name) {
return new Person(name);
}
}
[Sato(18), Tanaka(24)]
クラスに static な Function
を用意しておくことで、 Java 7 以下の環境でもメソッド参照のような感じで実装ができるようになる。
Target
List
を Set
に変換する方法として、 toSet()
というメソッドが用意されている。
import java.util.Set;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
Set<Integer> set = Lists.mutable
.of("one", "two", "three")
.collect(String::length)
.toSet();
System.out.println(set);
}
}
[3, 5]
この方法は単純なのだけど、デメリットとして collect()
したときと toSet()
したときで2度ループが回ってしまう。
もし1回のループの中で、 collect()
と Set
への置換えを一緒にやりたい場合は、 Target を指定できるメソッドを使用する。
import java.util.Set;
import com.gs.collections.impl.factory.Lists;
import com.gs.collections.impl.factory.Sets;
public class Main {
public static void main(String... args) throws Exception {
Set<Integer> set = Lists.mutable
.of("one", "two", "three")
.collect(String::length, Sets.mutable.of());
System.out.println(set);
}
}
[3, 5]
Target は、イテレーションメソッドの第二引数に、置き換え先コレクションのインスタンスを渡すことで指定できる。
こうすることで、 collect()
でループが回っているときに合わせて第二引数で指定したコレクションに値が順次セットされるようになる。
groupByEach()
import java.util.Arrays;
import java.util.List;
public class Person {
private String name;
private List<String> foods;
public Person(String name) {
this.name = name;
}
public Person likes(String... values) {
this.foods = Arrays.asList(values);
return this;
}
public List<String> getFavoriteFoods() {
return foods;
}
@Override
public String toString() {
return this.name;
}
}
import com.gs.collections.api.multimap.Multimap;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
Multimap<String, Person> map =
Lists.mutable
.of(
person("Sato").likes("rice", "bread", "fish"),
person("Suzuki").likes("bread", "ramen"),
person("Tanaka").likes("rice", "ramen")
)
.groupByEach(Person::getFavoriteFoods);
System.out.println(map);
}
private static Person person(String name) {
return new Person(name);
}
}
{ramen=[Suzuki, Tanaka], rice=[Sato, Tanaka], bread=[Sato, Suzuki], fish=[Sato]}
-
groupBy()
は、単一の値で集約していた。 -
groupByEach()
は、引数で渡したFunction
からIterable
を受け取り、要素1つ1つで集約を行う。
fixedList
Lists.mutable
、 Lists.immutable
と並んで IDE の補完候補に出てくる Lists.fixedList
。
「要素数が数個で固定されていて、徹底的にメモリ使用量を減らしたい」ときに使うらしい。
例えば、次のように fixedList
を定義した場合。
Lists.fixedList.of("aaa", "bbb");
この of()
の実装は次のようになっている。
public <T> FixedSizeList<T> of(T one, T two)
{
return this.with(one, two);
}
public <T> FixedSizeList<T> with(T one, T two)
{
return new DoubletonList<T>(one, two);
}
DoubletonList
というクラスを生成している。
これの実装を見に行くと、、、
@NotThreadSafe
final class DoubletonList<T> extends AbstractMemoryEfficientMutableList<T> implements Externalizable
{
private static final long serialVersionUID = 1L;
private T element1;
private T element2;
DoubletonList(T obj1, T obj2)
{
this.element1 = obj1;
this.element2 = obj2;
}
もはや要素は配列すら使わず保持されている。
配列の生成に使われるメモリすら惜しい場合に使うということか・・・。
要素数は宣言したときのサイズで固定されているので、要素の追加や削除はできない(実行時に例外がスローされる)。
要素の上書きはできる(immutable ではない)。
ちなみに、 Lists.fixedSize.of()
には可変長引数を受け取るものも定義されている。
その場合はどうなっているかというと、、、
public <T> FixedSizeList<T> with(T... items)
{
switch (items.length)
{
case 0:
return this.of();
case 1:
return this.of(items[0]);
case 2:
return this.of(items[0], items[1]);
case 3:
return this.of(items[0], items[1], items[2]);
case 4:
return this.of(items[0], items[1], items[2], items[3]);
case 5:
return this.of(items[0], items[1], items[2], items[3], items[4]);
case 6:
return this.of(items[0], items[1], items[2], items[3], items[4], items[5]);
default:
return ArrayAdapter.newArrayWith(items);
}
}
引数が6個より多くなったら、 ArrayAdapter
を使う処理に切り替わっている。
こいつは、内部で配列を使って値を保持している。
なので、本気でメモリ使用量を抑えたいなら、要素数は 6 個以下にしたほうがいいのだろう。
ArrayAdapter, ArrayIterate, ListAdapter, ListIterate
配列や SDK のコレクションを、 GS Collections のコレクションに変換するためのクラス。
主に、既存コードに GS Collections を導入するときの橋渡しとして利用する。
import com.gs.collections.impl.list.fixed.ArrayAdapter;
import com.gs.collections.impl.utility.ArrayIterate;
public class Main {
public static void main(String... args) throws Exception {
String[] array = {"one", "two", "three"};
ArrayAdapter.adapt(array)
.select(s -> s.contains("e"))
.collect(s -> "<" + s + ">")
.each(System.out::println);
ArrayIterate.select(array, s -> s.contains("o"))
.collect(s -> "[" + s + "]")
.each(System.out::println);
}
}
<one>
<three>
[one]
[two]
-
ArrayAdapter#adapt()
で、配列を GS Collections のコレクションに変換できる。 -
ArrayIterate
は、変換の1ステップを飛ばしていきなり GS Collections のメソッドを使える。 - 変換するだけなら
ArrayAdapter
を使い、それ以外の場合は好きな方を選ぶ感じ(個人的にはAdapter
の方が好きかもしれない)。
ListAdapter
と ListIterate
も同じ。
import java.util.Arrays;
import java.util.List;
import com.gs.collections.impl.list.mutable.ListAdapter;
import com.gs.collections.impl.utility.ListIterate;
public class Main {
public static void main(String... args) throws Exception {
List<String> list = Arrays.asList("one", "two", "three");
ListAdapter.adapt(list)
.select(s -> s.contains("e"))
.collect(s -> "<" + s + ">")
.each(System.out::println);
ListIterate.select(list, s -> s.contains("o"))
.collect(s -> "[" + s + "]")
.each(System.out::println);
}
}
<one>
<three>
[one]
[two]
チェック例外用のユーティリティ
LT で急遽紹介されたユーティリティ。
import java.io.File;
import java.nio.file.Files;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
Lists.mutable
.of(new File("hoge.txt"), new File("fuga.txt"))
.collect(File::toPath)
.flatCollect(path -> {
try {
return Files.readAllLines(path);
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.each(System.out::println);
}
}
hoge.txt
と fuga.txt
を読み込んで、全行出力しようとしているけど、 Files#readAllLines()
が IOException
をスローするせいでラムダ式が非常に残念なことになっている。
これに、 GS Collections が提供するユーティリティを使うと。。。
import java.io.File;
import java.nio.file.Files;
import static com.gs.collections.impl.block.factory.Functions.*;
import com.gs.collections.impl.factory.Lists;
public class Main {
public static void main(String... args) throws Exception {
Lists.mutable
.of(new File("hoge.txt"), new File("fuga.txt"))
.collect(File::toPath)
.flatCollect(throwing(Files::readAllLines))
.each(System.out::println);
}
}
Functions#throwing()
を間にかますことで、 try-catch
を書かなくて良くなる!
もしチェック例外がスローされると、 Functions#throwing()
は発生した例外を RuntimeException
に詰めてスローし直す。
ところで、この実装を Eclipse でやろうとすると、エディタ上はコンパイルエラーが発生する。
でも、 Gradle とかでビルドして動かすと、普通に動いてくれる。
IntelliJ だとエラーにならない。。。
Eclipse がんばれ。。。
プリミティブのコレクションは自動生成
GS Collections には、 DoubleArrayList
とか IntArrayList
とか、プリミティブ型の値をオートボクシングせずにそのまま保持するコレクションが用意されている。
これらのコードは、 Double
とか Int
のところだけ差し替えれば、後は全部同じ実装になっている。
なので、これらのクラスはテンプレートを使ってビルド時に動的に生成されている。
テンプレートのファイルは gs-collections-code-generator
の下の、 src/main/resources の下 にある。
例えば、 *ArraryList
のテンプレートは以下のようになっている。
@NotThreadSafe
public class <name>ArrayList extends Abstract<name>Iterable
implements Mutable<name>List, Externalizable
{
private static final long serialVersionUID = 1L;
private static final <type>[] DEFAULT_SIZED_EMPTY_ARRAY = {};
private static final <type>[] ZERO_SIZED_ARRAY = {};
private static final int MAXIMUM_ARRAY_SIZE = Integer.MAX_VALUE - 8;
protected int size;
protected transient <type>[] items = DEFAULT_SIZED_EMPTY_ARRAY;
public <name>ArrayList()
{
}
<name>
のところに Double
とか Int
が入って、 <type>
のところに double
とか int
が入るのだと思う。
その他所感等
- 何をどうすれば良いか分からない場面があったので、参加して直接ご質問できて良かったです。
- Kata は、 GS Collections だけでなく、ラムダ式やメソッド参照に慣れるための教材としても良さそうだと思いました。
- 個人的にメソッド参照を使うクセが付いておらず、ついラムダ式でダラダラ書こうとしてしまうので、今回のハンズオンでメソッド参照使うクセがついてきた感じがしました。
(たぶん、 Eclipse にメソッド参照の補完がないのが原因かと。 IntelliJ にちょっと興味を持ち出した。。。) - コメントを日本語化したら、社内での勉強会でも利用できそうだなぁと思ったりしたり。
- 個人的にメソッド参照を使うクセが付いておらず、ついラムダ式でダラダラ書こうとしてしまうので、今回のハンズオンでメソッド参照使うクセがついてきた感じがしました。
- 上記にまとめたように、新たに得られたものが多く、とても良い経験ができました。