Google Guava の transform, filter 系のお話

Java 派な皆様は本日も Google Guava の恩恵を受けていることかと思われます。

ある時自社プロダクトのパフォーマンスを調べていて、プロファイルを取ったところLists.transform が上位に出てきてました。


調べてみると、Lists.transform は遅延評価で、リストの要素を触る度に transform 処理が行われることがわかりました。

たとえば、下記コード。

List<Integer> values = ...

List<String> strings = Lists.transform(values, new Function<Integer, String>() {
    @Override
    public String apply(Integer i) {
        return doSomething(i);
    }
});

これは values の各要素に対して第二引数の function を適用したリストを返すコードに見えますが、
Lists.transform を呼んだ時点では function は適用されないのです。
一見、下記コードとほぼ同等であることを期待しますが、全く違います。

List<Integer> values = ...

List<String> strings = Lists.newArrayList();
for(Integer i : values){
    strings.add(doSomething(i));
}


ではいつ各要素に、transform の第二引数の function が適用されるのでしょう?リストの要素を触った時です。
下記コードがわかりやすいでしょう。

List<Integer> values = Lists.newArrayList(1, 2, 3);

List<String> strings = Lists.transform(values, new Function<Integer, String>() {
    @Override
    public String apply(Integer i) {
        System.out.println("convert " + i);
        return String.valueOf(i);
    }
});

System.out.println(strings);
System.out.println(strings);

// 出力結果
convert 1
convert 2
convert 3
[1, 2, 3]
convert 1
convert 2
convert 3
[1, 2, 3]

なんと、同じ要素に対して複数回 function が適用されています。for ループや、List#get で要素を取得した時にも function が適用されます。
従って、function の処理に時間がかかるとして、 Lists.transform で得られたリストを複数回ループしたり get(index) したりすると何度も function が適用され、アプリケーションのパフォーマンスに影響が出ます。

これは Sets.filter や Maps.asMap, Collections2.transform や Collection2.filter も同様の挙動です。


関数型チックに書いて満足してもパフォーマンスは劣化してますよ、みたいなことにならないよう気をつけましょうというお話でした。
現場からは以上です。

Java 最強リファレンス

Java 最強リファレンス