Java开发人员通常处理ArrayList和HashSet等集合。Java 8附带了lambda和streaming API,帮助我们轻松处理集合。在大多数情况下,我们只处理几千个条目的集合,而性能并不重要。但是,在某些极端的情况下,当我们不得不多次超过数百万件条目的集合时,性能就会变得很糟糕。
我使用JMH检查每个代码段的运行时间。
迭代是一个基本特性。所有编程语言都有简单的语法,允许程序员在集合中进行迭代。而 streaming API可以以非常简单的方式对集合进行迭代。
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 迭代代码很简单:
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语言风格代码比较冗长,但仍然非常紧凑:
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;
}
然后,查看性能比较:
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风格的循环代码,JVM只增加一个整数,然后直接从内存中读取值。这使它运行效率非常快。
但是forEach是非常不同的,根据从StackOverFlow和Oracle文档上获得的答案,JVM必须将forEach转换为迭代器,并对每个条目调用hasNext()。
这就是为什么forEach比C语言风格代码慢。
我们定义测试数据:
@State(Scope.Benchmark)
public static class BenchMarkState {
@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);
}
public Set<Integer> testData = new HashSet<>(500000);
}
Java集还支持流API和forEach循环。根据前面的测试,如果我们将Set转换为ArrayList,然后遍历ArrayList,性能可能会提高吗?
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;
}
将迭代器与C风格的循环的组合在一起如何呢?
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;
}
或者,只是简单的循环?
public List<Integer> forEach(BenchMarkState state){
List<Integer> result = new ArrayList<>(state.testData.size());
for(Integer item : state.testData) {
result.add(item);
}
return result;
}
这是一个很好的想法,但是它不起作用,因为初始化新的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使用HashMap)不是为迭代所有项而设计的。遍历HashMap的最快方法是将Iterator和C样式的循环结合起来,因为JVM不必调用hasNext()。
Foreach和Stream API可以方便地处理集合。您可以更快地编写代码。但是,当您的系统对稳定和性能要求很高时,您应该考虑编写合适的循环代码。