最近,工作中需要解析大文件进行数据加工处理,其中就涉及了对于大量数据的遍历操作,如下需嵌套遍历两个集合:
//从文件中获取数据,fifContent数量为:55W;sevenContent数量为:8K
List<Map<String, String>> fifContent = getFileContent( fifData );
List<Map<String, String>> sevenContent = getFileContent( sevenData );
.....
//嵌套for循环遍历,外层for:sevenContent,内层for:fifContent
for (Map<String, String> sevenMap : sevenContent) {
String code = sevenMap.get( "code" );
//使用并发流
fifContent.parallelStream().forEach( new Consumer<Map<String, String>>() {
@Override
public void accept(Map<String, String> stringStringMap) {
//模拟业务处理
}
} );
}
结果,运行起来相当的慢,耗时大约:446919ms (大约7分多钟)
经过分析,外层循环需遍历的数量为8K,内层循环需遍历的数量为55W,嵌套起来需要遍历的量级为:44亿 。。。
如此庞大的数据量,遍历起来不慢才怪,也曾尝试使用线程池来代替并发流,但是效果也不佳,因为本质上没有解决遍历次数的问题,此处不展开。
思考。。。。
从业务处理逻辑出发,能否避免使用嵌套循环了,如果使用一个或者多个单层循环是否也能解决问题呢?
业务逻辑:
利用外层集合中的code查找内层集合中是否存在包含此code的记录,如果包含则拿出内层集合中的记录进行加工处理;
由于思维惯性和不好的编码习惯,导致第一反应就是使用嵌套循环来实现,若数据量不大还好,数据很大的话就会变得很慢。
经分析,实际上可以先只遍历外层集合取出code放到一个Set或者List集合中,然后遍历内层集合,判断内层集合字段是否被Set集合包含,包含的话就可以继续向下处理。。
优化后的代码如下:
Set<String>codes = new HashSet<>();
//遍历集合:sevenContent(8k)
for (Map<String, String> sevenMap : sevenContent) {
codes.add( sevenMap.get("code"));
}
//再遍历fifContent(55w),这样就不需要嵌套循环,大大降低了时间复杂度
fifContent.parallelStream().forEach( new Consumer<Map<String, String>>() {
@Override
public void accept(Map<String, String> stringStringMap) {
String no = stringStringMap.get( "no" );
if(codes.contains(no)){
//模拟业务处理。。。
}
}
} );
本次运行耗时:313ms
使得效率提升的在于数据量级,请看两块代码遍历数量对比:
嵌套循环:8K * 55W
多个单循环:8K + 55W
除了上述的场景,还遇到另外一个嵌套循环场景:
/**嵌套循环的方式*/
Map<String,List<String>>testMap = new HashMap<>( );
List<Map<String, String>> testList = new ArrayList<Map<String, String>>();
//用testMap的key值 查找testList的元素,如果一致则进行业务处理
for (Map.Entry<String, List<String>> resultEntry : testMap.entrySet()) {
String keyCode = resultEntry.getKey();
List<String> valueIds = resultEntry.getValue();
for (Map<String, String> subMap : testList) {
if(keyCode.equals(subMap.get("no"))){
//模拟业务处理。。。
}
}
}
思考:如何不用嵌套循环?
选择方式:将testList转换成Map,将no字段作为map的key,这样遍历testMap集合时,进行testList2Map.get(keyCode)便可以;
//将testList 转换成 testList2Map
HashMap<String, Map<String,String>> testList2Map =
testList.parallelStream().collect(
HashMap::new,
(m, n) -> m.put( n.get( "bondCode" ), n ),
HashMap::putAll );
//仅需单个for循环遍历即可完成
for (Map.Entry<String, List<String>> resultEntry : testMap.entrySet()) {
String keyCode = resultEntry.getKey();
List<String> valueIds = resultEntry.getValue();
Map<String, String> valueMap = testList2Map.get( keyBondCode );
if(valueMap != null){
//模拟业务处理。。。
}
}
思考总结:
1.使用嵌套for循环之前关注一下数据量,是否可能会带来性能问题
2.for循环遍历可考虑使用stream流或parallelStream来提升性能
3.嵌套for循环是否可以从业务逻辑上避免,如将其中一层集合遍历转换为Set或Map来取值使用,而不是逐个遍历取值。