一.起因
由于最近业务的数据量和复杂度有点高,所有对效率这方便需要格外重视,不然代码是写完了,效率太低了,代码中使用到了非常多的循环,所以我折腾了几种循环的性能问题
二.对几种常见的做法进行分析
- 这里先附上测试代码
package com.xx.controller;
import com.xx.entity.Department;
import com.xx.entity.User;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author aqi
* DateTime: 2020/8/19 9:42 上午
* Description: No Description
*/
public class Demo {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
User user = new User();
user.setId(i);
user.setName("name:" + i);
String departmentId = i % 3 == 1 ? "1000" : (i % 3 == 2 ? "2000" : "3000");
user.setDepartmentId(departmentId);
userList.add(user);
}
List<Department> departmentList = new ArrayList<>();
departmentList.add(new Department("1000", "部门1"));
departmentList.add(new Department("2000", "部门2"));
departmentList.add(new Department("3000", "部门3"));
long start1 = System.currentTimeMillis();
// 增强for
// O(n * n)
for (User user : userList) {
for (Department department : departmentList) {
if (Objects.equals(user.getDepartmentId(), department.getId())) {
user.setDepartmentName(department.getDepartmentName());
break;
}
}
}
long end1 = System.currentTimeMillis();
System.out.println("增强for耗时:" + (end1 - start1));
// 普通for
// O(n * n)
for (int i = 0; i < userList.size(); i++) {
for (int j = 0; j < departmentList.size(); j++) {
if (Objects.equals(userList.get(i).getDepartmentId(), departmentList.get(j).getId())) {
userList.get(i).setDepartmentName(departmentList.get(j).getDepartmentName());
break;
}
}
}
long end2 = System.currentTimeMillis();
System.out.println("普通for耗时:" + (end2 - end1));
// 第一种stream写法,这个时间复杂度是O(n*n)
// O(n * n)
userList.forEach(e -> departmentList.forEach(ex -> {
if (Objects.equals(e.getDepartmentId(), ex.getId())) {
e.setDepartmentName(ex.getDepartmentName());
}
}));
long end3 = System.currentTimeMillis();
System.out.println("第一种stream写法耗时:" + (end3 - end2));
// 第二种stream写法O(n + n + 1) = O(n)
// O(n)
Map<String, Department> collect = departmentList.stream().collect(Collectors.toMap(Department::getId, Function.identity()));
// O(n)
userList.forEach(e -> {
// O(1)
Department department = collect.get(e.getDepartmentId());
e.setDepartmentName(department.getDepartmentName());
});
long end4 = System.currentTimeMillis();
System.out.println("第二种stream写法耗时:" + (end4 - end3));
}
}
- 查看测试结果
- 1w条数据
- 10w条数据
- 50w条数据
- 100w条数据
- 500w条数据
- 1000w条数据
这里列个表格,方便分析(时间单位ms)
数量 | for循环 | 增强for循环 | 第一种写法 | 第二种写法 |
---|---|---|---|---|
1w | 7 | 9 | 90 | 12 |
10w | 21 | 24 | 101 | 24 |
50w | 33 | 41 | 86 | 25 |
100w | 46 | 76 | 101 | 45 |
500w | 159 | 403 | 207 | 116 |
1000w | 299 | 1053 | 324 | 210 |
- 分析结果(这里使用的是ArrayList进行的测试,并没有对LinkedList进行测试,并且测试结果也可能没有那么的精确,这里采用10次求平均值的方式 )
- 这里我们发现普通for的执行效率是比增强for效率要高的,并且随着数据量的增大,性能差距越明显
- 随着数据量的不断增多,增强for的性能急剧下降,普通for循环的下滑并没有那么严重
- 后面两种都是采用stream流处理的方式进行的,但是第一种写法在数据量不大的情况下,效率似乎有些太差了,不过随着数据量的增加,执行耗时并没有太大的变化,应该主要是创建流比较耗时
- 第二种写法整体效率和普通for循环差不多,并且在数据量偏大的时候性能稍优于普通for循环
总结:普通for循环的执行效率和第二种写法的效率远高于其他两种,增强for循环性能稍弱,但效率随数据量变多效率下滑十分严重,第一种写法数据量不多时效率偏低,但是数据量较大是效率还算可以
三.分析原因
- 先从两种传统的for循环开始分析,因为都是二重循环,所以时间复杂度都是O(n * n),理论上时间应该差不了太多,但是我们只要看一下这两者的源码就可以知道为什么了
-
左边的是fori循环,可以看到只进行了一个数组下标是否越界的判断就,直接返回了这个数据
-
右边的是增强for循环,可以看到在获取数据之前进行了许多的判断,再返回数据
- 第一种写法的时间复杂度是O(n * n),而第二种写法的时间复杂度是O(n + n) = O(n),可以看出第二种写法的时间复杂度是最小的,但是因为是流处理,所以要创建流,这就导致在数据量小的时候流处理并不占优势,甚至效率十分的低下,但是由于第二种写法将时间复杂度降到了O(n)这才大幅度减少了其耗时
这里附上折线图,便于观察
四.总结
- 有些人偏爱使用stream流的形式去遍历集合(比方说我自己),但是可以看到在数据量不大的情况下,效率是偏低的,但是虽然说偏低,但也是毫秒级别的,只是相较于传统的for循环效率偏低(stream.foreach好像没有办法跳出嵌套循环,不知道有没有谁知道可以怎么做,这样可以减少不必要的循环次数)
- 并且在需要使用stream去进行嵌套循环的时候,推荐使用第二种写法,这种写法使用HashMap查找数据时间复杂度为O(1)的特点,降低了时间复杂度,并且不论在数据量多少的情况下,效率都十分的稳定,爱了爱了,这里我本人是比较推荐这种写法的
- 这里的增强for在大于100w数据的情况下性能开始极端下降,这个是需要十分注意的
- 传统for循环的性能也是十分稳定的,但是其使用的时候需要些很多的i,j,k类似的下标参数,这样的代码看起来会比较的混乱,代码写出来也会偏长,我不是很喜欢用,看各自喜好吧