Lists.transformとは
Lists.transformは便利なメソッドです。
あるリストの各要素を変換して別のリストを生成したい時に使えます。
関数型言語界隈だとよくmapという名前で定義されているやつです。
例えば、
List<Integer> nums = Lists.newArrayList(1,2,3);
List<String> stringNums = Lists.transform(nums, new Function<Integer, String>() {
@Override
public String apply(@Nullable Integer arg0) {
return String.valueOf(arg0);
}
});
とすると、numsの各要素にtransformの第2引数で渡した関数(以下、「変換関数」と呼ぶ)を適用した結果のリストを返してくれます。
変換関数は遅延評価される
このメソッドは色々な所で使い所があるのですが、使用する上で1つ注意する必要がある事があります。
それは、transformの結果返ってくるリストは、内部的にはTransformingRandomAccessListかTransformingSequentialListというものが返ってきています。
これらのリストは、その中の要素を取得する際に変換関数を適用するように実装されています。
つまり、先のサンプル中のstringNumsのイテレータを回したり、getメソッドを呼ぶと、そのたびに変換関数が元のリストの要素に対して適用されて、変換後の値が返されています。
なので、変換関数が非常に軽い処理しかしていなかったり、変換関数のコストが高くても一度しかその中の値を使わないのであれば、問題が起きることはありませんが
- 変換関数で行なっている処理が重い(重い計算をする、DBアクセスする、通信が発生する等)
- transformの結果返ってくるリストを何度もイテレータを回す
- 変換後のリストに対してcontainsやhashCodeのようなを内部的に要素を舐めるようなメソッド実行する
という場合は、transformが返すリストをそのまま使いまわすのはコストの観点からよろしくありません。
対処法
実は、transformのjavadocにもそのことはちゃんと書いてあります。
The function is applied lazily, invoked when needed. This is necessary
for the returned list to be a view, but it means that the function will be
applied many times for bulk operations like {@link List#contains} and
{@link List#hashCode}. For this to perform well, {@code function} should be
fast. To avoid lazy evaluation when the returned list doesn't need to be a
view, copy the returned list into a new list of your choosing.
つまり、変換関数は軽い処理をするべきだが、もし遅延評価が嫌なのであれば、transformが返すリストをコピーして使うべしという事です。
なぜこのような実装になっているのか
javadocを読むと、
Returns a list that applies {@code function} to each element of {@code
fromList}. The returned list is a transformed view of {@code fromList};
changes to {@code fromList} will be reflected in the returned list and vice
versa.
とあります。
このメソッド自体が変換後のリストを返すのではなく、変換されたリストに見えるもの(transformed view)を返すためのメソッドだということですね。
その証拠に変換元のリストに何か変更を加えたら、変換後のメソッドにもちゃんと反映されると。
List<Integer> nums = Lists.newArrayList(1,2,3);
List<String> stringNums = Lists.transform(nums, new Function<Integer, String>() {
@Override
public String apply(@Nullable Integer arg0) {
return String.valueOf(arg0);
}
});
nums.add(100);
assertTrue(stringNums.contains("100")); // transformの後にaddした要素もちゃんと変換されている!!
結論
javadoc読め。