牛X,新同事把代码耗时从 26856ms 优化到了 748ms

文化   2024-11-28 09:09   湖北  

在两份表里找相同id的数据,很多同学会写两个for循环嵌套。这个写法效率比较低,今天来看一个提高速度的优化案例。

本篇分析的技巧点其实是比较常见的,但是最近的几次的代码评审还是发现有不少兄弟没注意到。所以还是想拿出来说下。

是个什么场景呢?就是 for循环 里面还有 for循环, 然后做一些数据匹配、处理 这种场景。

我们结合实例代码来看看。场景示例:

比如我们现在拿到两个list 数据 ,一个是 User List 集合 ;另一个是 UserMemo List集合;

我们需要遍历 User List ,然后根据 userId 从 UserMemo List 里面取出 对应这个userId 的 content 值,做数据处理。

代码 User.java :

@Data
public class User {
    private Long userId;
    private String name;
}

代码 UserMemo.java :

@Data
public class UserMemo {
    private Long userId;
    private String content;
}

模拟数据集合 :5W 条 user 数据 , 3W条 userMemo数据

public static List<User> getUserTestList() {
        List<User> users = new ArrayList<>();
        for (int i = 1; i <= 50000; i++) {
            User user = new User();
            user.setName(UUID.randomUUID().toString());
            user.setUserId((long) i);
            users.add(user);
        }
        return users;
    }

    public static List<UserMemo> getUserMemoTestList() {
        List<UserMemo> userMemos = new ArrayList<>();
        for (int i = 30000; i >= 1; i--) {
            UserMemo userMemo = new UserMemo();
            userMemo.setContent(UUID.randomUUID().toString());
            userMemo.setUserId((long) i);
            userMemos.add(userMemo);
        }
        return userMemos;
    }

先看平时大家不注意的时候可能会这样去写代码处理 :

其实数据量小的话,其实没多大性能差别,不过我们还是需要知道一些技巧点。

我们来看看 这时候的一个耗时情况 。

相当于迭代了 5W * 3W 次 ,可以看到用时 是 26857毫秒 :

其实到这,插入个题外点,如果说每个userId 在 UserMemo List 里面 都是只有一条数据的场景。

for (User user : userTestList) {
    Long userId = user.getUserId();
    for (UserMemo userMemo : userMemoTestList) {
        if (userId.equals(userMemo.getUserId())) {
            String content = userMemo.getContent();
            System.out.println("模拟数据content 业务处理......"+content);
        }
    }
}

单从这段代码有没有问题 ,有没有优化点。显然是有的, 因为当我们从内循环UserMemo List里面找到匹配数据的时候, 没有做其他操作了。

这样内for循环会继续下,直到跑完再进行下一轮整体循环。所以,仅针对这种情形,1对1的或者说我们只需要找到一个匹配项,处理完后我们 应该使用 break 。

我们来看看加上 break 的一个耗时情况 :

耗时情况:可以看到 从 2W 多毫秒 变成了 1W 多毫秒, 这个break 加的很OK。

回到我们刚才, 平时需要for 循环里面再 for 循环 这种方式,可以看到耗时是 2万6千多毫秒。

那如果场景更复杂一定, 是for 循环里面 for循环 多个或者, for循环里面还有一层for 循环 ,那这样代码耗时真的非常恐怖。

那么接下来这个技巧点是使用map 去优化 :

代码:

    public static void main(String[] args) {
        List<User> userTestList = getUserTestList();
        List<UserMemo> userMemoTestList = getUserMemoTestList();

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        //使用stream() 记得一定要判空 这里没列出来,大家自己注意
        Map<Long, String> contentMap =
                userMemoTestList.stream().collect(Collectors.toMap(UserMemo::getUserId, UserMemo::getContent));

        for (User user : userTestList) {
            Long userId = user.getUserId();
            String content = contentMap.get(userId);

            if (StringUtils.hasLength(content)) {
                System.out.println("模拟数据content 业务处理......" + content);
            }

        }

        stopWatch.stop();
        System.out.println("最终耗时" + stopWatch.getTotalTimeMillis());

    }

看看耗时:

为什么效果这么显著?

这其实就是时间复杂度,for循环嵌套for循环,就好比 循环每一个 user ,拿出 userId 需要在里面的循环从 userMemo list集合里面 按顺序去开盲盒匹配,拿出第一个,看看userId ,拿出第二个,看看userId ,一直找匹配的。

而我们提前对 userMemo list集合 做一次 遍历,转存储在map里面 。

map的取值效率 在多数的情况下是能维持接近 O(1) 的 , 毕竟数据结构摆着,数组加链表。

相当于拿到userId  想去开盲盒的时候, 根据userId 这个key hash完能直接找到数组里面的索引标记位, 如果底下没链表(有的话O(logN)),直接取出来就完事了。

按照目前以JDK8 的hash算法,起hash冲突的情况是非常非常少见了。

最恶劣的情况,只有当 全部key 都冲突, 全都分配到一个桶里面去都占用一个位置 ,这时候就是O(n),这种情景不需要去考虑。

原文:blog.csdn.net/qq_35387940/article/details/129518893

👉最新2T+免费Java视频学习资料点击领取>>

END

精品资料,超赞福利,免费领


微信扫码/长按识别 添加【技术交流群
群内每天分享精品学习资料


最近开发整理了一个用于速刷面试题的小程序《面试手册》【点击使用】;其中收录了上千道常见面试题及答案(包含基础并发JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud消息队列等多个类型),欢迎您的使用。


在 Docker 中跑 MySQL?恭喜你,要下岗了
最强平替Postman的工具,拒绝浏览器套壳
甘露寺招聘公告!环境清幽,双休包吃住,午休3小时
IDEA 必装的 11 个 插件,开发效率提升 5 倍!
【原创】怒肝3W字Java学习路线!从入门到封神全包了(建议收藏)
程序员专属导航站(baoboxs.com),一站式工作、学习、娱乐!

👇👇

👇点击"阅读原文",领更多资料(更新中...

一行Java
专注JAVA;技术分享,讨论交流。
 最新文章