公司來了個新同事,把程式碼耗時從 26856ms 最佳化到了 748ms,一頓操作猛如虎!

Java技术栈發表於2024-11-05

在兩份表裡找相同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),這種情景不需要去考慮。

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

更多文章推薦:

1.Spring Boot 3.x 教程,太全了!

2.2,000+ 道 Java面試題及答案整理(2024最新版)

3.免費獲取 IDEA 啟用碼的 7 種方式(2024最新版)

覺得不錯,別忘了隨手點贊+轉發哦!

相關文章