原文出處:ImportNew
有許許多多關於 Java 8 中流效率的討論,但根據 Alex Zhitnitsky 的測試結果顯示:堅持使用傳統的 Java 編程風格——iterator 和 for-each 循環——比 Java 8 的實現性能更佳。
Java 8 中的 Lambda 表達式和流(Stream)受到了熱烈歡迎。這是 Java 迄今為止最令人激動的特征。這些新的語言特征允許采用函數式風格來進行編碼,我們可以用這些特性完成許多有趣的功能。這些特性如此有趣以至於被認為是不合理的。我們對此表示懷疑,於是決定對這些特性進行測試。
我們創建一個簡單的任務:從一個 ArrayList 找出最大值,將傳統方式與 Java 8 中的新方式進行測試比較。說實話,測試的結果讓我感到非常驚訝。
命令式風格與 Java 8 函數式編程風格比較
我喜歡直接進入主題,所以先看一下結果。為了做這次基准測試,我們先創建了一個 ArrayList,並插入一個 100000 個隨機整數,並通過 7 種不同的方式遍歷所有的值來查找最大值。實現分為兩組:Java 8 中引入的函數式風格與 Java 一直使用的命令式風格。
最大錯誤記錄是並行流上的 0.042,完整輸出結果在這篇文章結尾部分可以看到。
小貼士:
哇哦!Java 8 中提供的任何一種新方式都會產生約 5 倍的性能差異。有時使用簡單迭代器循環比混合 lambda 表達式和流更有效,即便這樣需要多寫幾行代碼,且需要跳過甜蜜的語法糖(syntactic suger)。
使用迭代器或 for-each 循環是遍歷 ArrayList 最有效的方式,性能比采用索引值的傳統 for 循環方式好兩倍。
在 Java 8 的方法中,並行流的性能最佳。但是請小心,在某些情況下它也可能會導致程序運行得更慢。
Lambda 表達式的速度介於流與並行流之間。這個結果確實挺令人驚訝的,因為 lambda 表達式的實現方式是基於流的 API 來實現的。
不是所有的情況都如上所示:當我們想演示在 lambda 表達式和流中很容易犯錯時,我們收到了很多社區的反饋,要求我們優化基准測試代碼,如消除整數的自動裝包和解包操作。第二次測試(已優化)的結果在這篇文章結束位置可以看到。
讓我們快速看一下每個方法,按照運行速度由快到慢:
命令式風格
iteratorMaxInteger()——使用迭代器遍歷列表:
1
2
3
4
5
6
7
|
public
int
iteratorMaxInteger() {
int
max = Integer.MIN_VALUE;
for
(Iterator it = integers.iterator(); it.hasNext(); ) {
max = Integer.max(max, it.next());
}
return
max;
}
|
forEachLoopMaxInteger()——不使用迭代器,使用 For-Each 循環遍歷列表(不要誤用 Java 8 的 forEach)
1
2
3
4
5
6
7
|
public
int
forEachLoopMaxInteger() {
int
max = Integer.MIN_VALUE;
for
(Integer n : integers) {
max = Integer.max(max, n);
}
return
max;
}
|
forMaxInteger()——使用簡單的 for 循環和索引遍歷列表:
1
2
3
4
5
6
7
|
public
int
forMaxInteger() {
int
max = Integer.MIN_VALUE;
for
(
int
i =
0
; i < size; i++) {
max = Integer.max(max, integers.get(i));
}
return
max;
}
|
函數式風格
parallelStreamMaxInteger()——使用 Java 8 並行流遍歷列表:
1
2
3
4
|
public
int
parallelStreamMaxInteger() {
Optional max = integers.parallelStream().reduce(Integer::max);
return
max.get();
}
|
lambdaMaxInteger()——使用 lambda 表達式及流遍歷列表。優雅的一行代碼:
1
2
3
|
public
int
lambdaMaxInteger() {
return
integers.stream().reduce(Integer.MIN_VALUE, (a, b) -> Integer.max(a, b));
}
|
forEachLambdaMaxInteger()——這個用例有點混亂。可能是因為 Java 8 的 forEach 特性有一個很煩人的東西:只能使用 final 變量,所以我們創建一個 final 包裝類來解決該問題,這樣我們就能訪問到更新后的最大值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
int
forEachLambdaMaxInteger() {
final
Wrapper wrapper =
new
Wrapper();
wrapper.inner = Integer.MIN_VALUE;
integers.forEach(i -> helper(i, wrapper));
return
wrapper.inner.intValue();
}
public
static
class
Wrapper {
public
Integer inner;
}
private
int
helper(
int
i, Wrapper wrapper) {
wrapper.inner = Math.max(i, wrapper.inner);
return
wrapper.inner;
}
|
順便提一下,如果要討論 forEach,我們提供了一些有趣的關於它的缺點的見解,答案參見StackOverflow。
streamMaxInteger()——使用 Java 8 的流遍歷列表:
1
2
3
4
|
public
int
streamMaxInteger() {
Optional max = integers.stream().reduce(Integer::max);
return
max.get();
}
|
優化后的基准測試
根據這篇文章的反饋,我們創建另一個版本的基准測試。源代碼的不同之處可以在這里查看。下面是測試結果:
最后的思考
開始使用 Java 8 的第一件事情是在實踐中使用 lambda 表達式和流。但是請記住:它確實非常好,好到可能會讓你上癮!但是,我們也看到了,使用傳統迭代器和 for-each 循環的 Java 編程風格比 Java 8 中的新方式性能高很多。
當然,這也不是絕對的。但這確實是一個相當常見的例子,它顯示可能會有大約 5 倍的性能差距。如果這影響到系統的核心功能或成為系統一個新的瓶頸,那就相當可怕了。