在兩份表裡找相同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最新版)
覺得不錯,別忘了隨手點贊+轉發哦!