专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
图虫APP  ·  OpenSeeGallery|刘树奎:穿行在 ... ·  19 小时前  
图虫APP  ·  投稿截止倒计时3天|​2024图虫OPENS ... ·  2 天前  
图虫APP  ·  OpenSeeGallery|朴容贤-刘小龙 ... ·  2 天前  
摸摸艺术书  ·  超高性价比,这本小书藏着肖像画的终极答案 ·  3 天前  
51好读  ›  专栏  ›  Java基基

公司来了个新同事,把代码耗时从 26856ms 优化到了 748ms,一顿操作猛如虎!

Java基基  · 公众号  ·  · 2024-12-24 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本

来源:blog.csdn.net/qq_35387940
/article/details/129518893


前言

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

所以还是想拿出来说下。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 视频教程:https://doc.iocoder.cn/video/

正文

是个什么场景呢?

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

我们结合实例代码来看看。

场景示例:

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

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

代码  User.java :

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

代码 UserMemo.java :

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

模拟 数据集合 :

5W 条 user 数据 , 3W条 userMemo数据

public static List getUserTestList() {
    List 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 getUserMemoTestList() {
    List 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;
}

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

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

代码:

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


    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    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);
            }
        }
    }


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

}

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

相当于迭代了 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 的一个耗时情况 :

代码:

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


    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    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);
                break;
            }
        }
    }


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


}

耗时情况:

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

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

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

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

代码:

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


    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    Map 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)),直接取出来就完事了。

然后补充一个getNode的代码注释 :







请到「今天看啥」查看全文