高效遍历Java容器
通過(guò)本文,你可以更深入的學(xué)習(xí) Java 語(yǔ)言中 forEach 語(yǔ)法的知識(shí),以及它和 C 語(yǔ)言形式的 for 循環(huán)、 Steam API 的對(duì)比。
?
簡(jiǎn)介
Java 程序員經(jīng)常使用容器,比如 ArrayList 和 HashSet。Java 8 中的 lambda 語(yǔ)法和 steaming API 可以讓我們更方便的使用容器。大部分情況下,我們僅僅處理幾千個(gè)元素,也不會(huì)去考慮性能問(wèn)題。但是,在一些極端場(chǎng)景下,如果我們需要遍歷上百萬(wàn)個(gè)元素,性能問(wèn)題就凸顯出來(lái)了。
本文將采用 JMH 計(jì)算每塊代碼的運(yùn)行時(shí)間。
?
forEach vs. C Style vs. Stream API
遍歷是一個(gè)基本的功能。所有編程語(yǔ)言都提供了簡(jiǎn)單的語(yǔ)法,讓程序員去遍歷容器。
Steam API 以一種非常直接的形式來(lái)遍歷容器。
public?List<Integer> streamSingleThread(BenchMarkState state){List<Integer> result =?new?ArrayList<>(state.testData.size());state.testData.stream().forEach(item -> {result.add(item);});return?result; } public?List<Integer> streamMultiThread(BenchMarkState state){List<Integer> result =?new?ArrayList<>(state.testData.size());state.testData.stream().parallel().forEach(item -> {result.add(item);});return?result; }?
forEach 循環(huán)也很簡(jiǎn)單:
public?List<Integer>?forEach(BenchMarkState state){List<Integer> result =?new?ArrayList<>(state.testData.size());for(Integer item : state.testData){result.add(item);}return?result; }?
C 語(yǔ)言形式的 for 循環(huán)啰嗦一些,不過(guò)依然很緊湊:
public?List<Integer>?forCStyle(BenchMarkState state){int?size = state.testData.size();List<Integer> result =?new?ArrayList<>(size);for(int?j =?0; j < size; j ++){result.add(state.testData.get(j));}return?result; }?
以下是性能報(bào)告:
Benchmark ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Mode ?Cnt ? Score ? Error ?Units TestLoopPerformance.forCStyle ? ? ? ? ? avgt ?200??18.068?±?0.074??ms/op TestLoopPerformance.forEach?? ? ? ? ? ? avgt ?200??30.566?±?0.165??ms/op TestLoopPerformance.streamMultiThread ? avgt ?200??79.433?±?0.747??ms/op TestLoopPerformance.streamSingleThread ?avgt ?200??37.779?±?0.485??ms/op?
使用 C 語(yǔ)言形式的 for 循環(huán),JVM 每次僅僅增加一個(gè)數(shù)字,然后直接從內(nèi)存里讀出數(shù)據(jù)。這使得它非常迅速。但是 forEach 就大不一樣,根據(jù) StackOverFlow 的這篇回答(https://stackoverflow.com/questions/85190/how-does-the-java-for-each-loop-work/85206#85206),和 Oracle 的文章(https://docs.oracle.com/javase/1.5.0/docs/guide/language/foreach.html),JVM 需要把 forEach 轉(zhuǎn)換成一個(gè) iterator,然后每個(gè)元素都調(diào)用一次 hasNext() 方法。這就是 forEach 比 C 語(yǔ)言的形式慢一些的原因。
?
哪一個(gè)是遍歷 Set 最高效的方法呢?
我們先定義測(cè)試數(shù)據(jù)集:
@State(Scope.Benchmark) public?static?class?BenchMarkState?{public?Set<Integer> testData =?new?HashSet<>(500000);@Setup(Level.Trial)public?void?doSetup()?{for(int?i =?0; i <?500000; i++){testData.add(Integer.valueOf(i));}}@TearDown(Level.Trial)public?void?doTearDown()?{testData =?new?HashSet<>(500000);}}?
Java 中的 Set 也支持 Steam API 和 forEach 循環(huán)。參考之前的測(cè)試,如果我們把 Set 轉(zhuǎn)換成 ArrayList,然后遍歷 ArrayList,或許性能會(huì)好一些?
public?List<Integer>?forCStyle(BenchMarkState state){int?size = state.testData.size();List<Integer> result =?new?ArrayList<>(size);Integer[] temp = (Integer[]) state.testData.toArray(new?Integer[size]);for(int?j =?0; j < size; j ++){result.add(temp[j]);}return?result; }?
如果把 iterator 和 C 語(yǔ)言形式結(jié)合起來(lái)呢?
public?List<Integer>?forCStyleWithIteration(BenchMarkState state){int?size = state.testData.size();List<Integer> result =?new?ArrayList<>(size);Iterator<Integer> iteration = state.testData.iterator();for(int?j =?0; j < size; j ++){result.add(iteration.next());}return?result;}?
或者,簡(jiǎn)單的遍歷怎么樣?
public?List<Integer>?forEach(BenchMarkState state){List<Integer> result =?new?ArrayList<>(state.testData.size());for(Integer item : state.testData) {result.add(item);}return?result; }?
這個(gè)主意不錯(cuò),不過(guò)它的效率也不高,因?yàn)槌跏蓟粋€(gè)新的 ArrayList 同樣需要消耗資源。
Benchmark ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Mode ?Cnt ?Score ? Error ?Units TestLoopPerformance.forCStyle ? ? ? ? ? ? ? avgt ?200??6.013?±?0.108??ms/op TestLoopPerformance.forCStyleWithIteration ?avgt ?200??4.281?±?0.049??ms/op TestLoopPerformance.forEach?? ? ? ? ? ? ? ? avgt ?200??4.498?±?0.026??ms/op?
HashMap (使用 HashMap<E,Object> 的 HashSet) 不是為遍歷所有元素設(shè)計(jì)的。遍歷一個(gè) HashMap 最快的方法是把 Iterator 和 C 語(yǔ)言形式結(jié)合起來(lái),這樣 JVM 就不會(huì)去調(diào)用 hasNext()。
?
結(jié)論
Foreach 和 Steam API 用來(lái)處理集合是很方便的。你可以更快的寫代碼。不過(guò),如果你的系統(tǒng)很穩(wěn)定,性能是一個(gè)主要的考量,你應(yīng)該考慮一下重寫你的循環(huán)。
總結(jié)
以上是生活随笔為你收集整理的高效遍历Java容器的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 阅读开源源码的正确姿势建议
- 下一篇: 基于consul实现微服务的服务发现和负