はじめに
よく、次のようなコードを目にします。
employees.stream().map(e -> e.getName()).collect(Collectors.toList());
employees
というリストの要素をすべて名前に変換したリストを作成するコード。
ごく普通のコードですが、ラムダ式ってインナークラスみたいなものだから、オーバーヘッドがあるのではないか?
こんなgetterを呼ぶだけのものであれば、
employees.stream().map(Employee::getName).collect(Collectors.toList());
の方が速いんではないか?
と思って、確かめるべく、計測してみました。
(ちょっと古いが、 Oracle JDK 1.8u131で計測しました。)
メソッド参照か、ラムダ式か
ベンチマーク
JMHを使って、それぞれのコードの1秒あたりの実行回数(スループット)を計測してみました。
public class PerformanceMethods {
/** 10000個の要素を生成 */
private static final List<Employee> employees = IntStream.range(0, 10000)
.mapToObj(i -> new Employee(i, "name" + i))
.collect(Collectors.toList());
@Benchmark
public void useReference() {
employees.stream().map(Employee::getName).collect(Collectors.toList());
}
@Benchmark
public void useLambda() {
employees.stream().map(e -> e.getName()).collect(Collectors.toList());
}
}
結果は・・・
Benchmark Mode Cnt Score Error Units
PerformanceMethods.useLambda thrpt 5 5842.820 ± 65.662 ops/s
PerformanceMethods.useReference thrpt 5 5762.353 ± 343.302 ops/s
Scoreが大きいほど処理速度が速いのですが、結果はほとんど変わらず。
繰り返し実行するなら、どちらでもよい、ということになります。
ちなみに、参考として、ラムダ式の実行の仕組みが書かれた資料を貼っておきます。
https://www.slideshare.net/miyakawataku/lambda-meets-invokedynamic
通常、インナークラスを作るとコンパイル時にクラスファイルが生成されますが(「$」付きのクラス)、ラムダ式は実行時に初期化されるため、起動が速くなるようです。
逆に取ると、ラムダ式を実行する最初のタイミングのみ、初期化が行われる分だけ遅くなる、ということになります。
気にするほどでもないと思いますが、個人的にはすっきりしているメソッド参照を使用したいと思います。
ラムダ式1回か、map2回か
他人が作ったコードのレビューをしていると、こんなコードを見かけました。
employees.stream().map(Employee::getAddress).map(Address::getPostalCode).collect(Collectors.toList());
ネストしたエンティティの値のリストを作るときに、 map
を2回使って変換する方法を初めて見ました。。
確かに「個人的にはすっきりしているメソッド参照を使用したい」とは書きましたが、さすがに map
2回は効率が悪いのではないか・・・?
ということで、ラムダ式1つで map
1回で書く場合と、メソッド参照で map
2回で書く場合のパフォーマンスを計測してみました。
ベンチマーク
public class PerformanceMethods {
private static final List<Employee> employees = IntStream.range(0, 10000)
.mapToObj(i -> new Employee(i, "name" + i, new Address("code" + i)))
.collect(Collectors.toList());
@Benchmark
public void mapOnce() {
employees.stream()
.map(e -> e.getAddress().getCode())
.collect(Collectors.toList());
}
@Benchmark
public void mapTwice() {
employees.stream()
.map(Employee::getAddress)
.map(Address::getCode)
.collect(Collectors.toList());
}
}
結果は・・・
Benchmark Mode Cnt Score Error Units
PerformanceMethods.mapOnce thrpt 5 6340.454 ± 1291.055 ops/s
PerformanceMethods.mapTwice thrpt 5 5487.546 ± 488.373 ops/s
ラムダ式( mapOnce
)の方のError(処理時間のばらつき)の範囲が大きいのは気になりますが、やはり map
1回の方が速いことが分かりました。
map
1回でラムダ式を書いた方が分かりやすいし。。
結論
- メソッド参照とラムダ式で速度の差はほとんどなし。ただし、ラムダ式は実行時に初期化されるため、最初だけ少しオーバーヘッドあり。
-
map
2回でメソッド参照を用いて値を取るよりも、map
1回でラムダ式を使って書いた方が速い。そして、読みやすい。