成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Stream很好,Map很酷,但答應我別用toMap()!

開發 前端
toMap () 是一個強大的工具,但也是一個危險的工具。它的簡單易用性可能會掩蓋潛在的問題,導致代碼在生產環境中出現意想不到的錯誤。因此,在使用 toMap () 時,一定要謹慎處理重復鍵、null 值和性能問題,或者選擇更合適的替代方案。?

兄弟們,今天咱們來聊聊 Java Stream 里那個看似人畜無害的 toMap () 方法。這貨就像個表面乖巧的二哈,平時賣萌撒嬌樣樣在行,但一不留神就給你拆家 —— 哦不,是讓你的代碼在生產環境里炸鍋。

先別急著反駁,我知道你們肯定用過類似這樣的代碼:

List<User> users = ...;
Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity()));

這行代碼看起來人畜無害,甚至還挺優雅。但如果 users 里有兩個用戶的 id 相同呢?恭喜你,喜提IllegalStateException: Duplicate key異常一枚。這就好比你去餐廳點餐,服務員說 “我們這兒的漢堡包保證獨一無二”,結果端上來兩個一模一樣的,你說氣不氣?更坑的是,toMap () 的 “坑” 遠不止這一個。比如,當你想把 List 轉成 Map 時,發現 value 可能為 null,這時候 toMap () 會直接拋出 NPE,連個解釋的機會都不給你。再比如,當你在并行流中使用 toMap () 時,可能會遇到線程安全問題,導致數據混亂。這些問題就像隱藏在代碼里的定時炸彈,隨時可能讓你的程序 “boom” 的一聲爆炸。

一、toMap () 的三大致命傷

1. 重復鍵:雙胞胎鍵的世紀難題

(1)默認行為:一視同仁,直接炸毛

toMap () 的默認行為是,如果遇到重復的鍵,就直接拋出IllegalStateException。這就好比你在玩消消樂,好不容易湊齊三個相同的元素,結果游戲直接閃退了。這種設計在大多數情況下是合理的,因為 Map 的鍵必須唯一。但在實際開發中,數據重復的情況并不少見,比如從數據庫查詢數據時,可能會因為業務邏輯問題導致重復記錄。

舉個栗子:

List<Product> products = Arrays.asList(
    new Product(1L, "蘋果"),
    new Product(1L, "香蕉")
);
Map<Long, String> productMap = products.stream()
    .collect(Collectors.toMap(Product::getId, Product::getName));

這段代碼會拋出Duplicate key異常,因為兩個 Product 對象的 id 都是 1L。這時候,你可能會想:“我只是想保留最后一個出現的值,或者合并它們,難道就這么難嗎?”

(2)合并策略:教 toMap () 做人

為了應對重復鍵的問題,toMap () 提供了一個三參數的重載方法,允許你自定義合并策略。例如,你可以選擇保留舊值、替換新值,或者將兩個值合并。

  • 保留舊值:
Map<Long, String> productMap = products.stream()
    .collect(Collectors.toMap(
        Product::getId,
        Product::getName,
        (oldValue, newValue) -> oldValue // 保留舊值
    ));
  • 替換新值:
Map<Long, String> productMap = products.stream()
    .collect(Collectors.toMap(
        Product::getId,
        Product::getName,
        (oldValue, newValue) -> newValue // 替換新值
    ));
  • 合并值:
Map<Long, String> productMap = products.stream()
    .collect(Collectors.toMap(
        Product::getId,
        Product::getName,
        (oldValue, newValue) -> oldValue + "," + newValue // 合并值
    ));

這樣,當遇到重復鍵時,toMap () 就會按照你定義的合并策略來處理,而不是直接拋出異常。但問題來了,這種方法需要你在代碼中顯式處理重復鍵,增加了代碼的復雜性。而且,如果你的業務邏輯比較復雜,合并策略可能會變得難以維護。

2. null 值:隱形殺手

(1)鍵為 null:Map 的禁區

Map 的鍵是不允許為 null 的(HashMap 允許,但 ConcurrentHashMap 不允許)。如果你在使用 toMap () 時,某個元素的鍵映射結果為 null,就會拋出NullPointerException。

舉個栗子:

List<User> users = Arrays.asList(
    new User(null, "張三"),
    new User(1L, "李四")
);
Map<Long, String> userMap = users.stream()
    .collect(Collectors.toMap(User::getId, User::getName));

這段代碼會拋出NullPointerException,因為第一個 User 對象的 id 為 null。這時候,你可能會想:“我只是想過濾掉 id 為 null 的用戶,難道就這么難嗎?”

(2)值為 null:無聲的陷阱

Map 的值是允許為 null 的,但在某些情況下,值為 null 可能會導致后續操作出現問題。例如,當你使用map.get(key)獲取值時,如果值為 null,就需要進行判空處理。

舉個栗子:

List<User> users = Arrays.asList(
    new User(1L, null),
    new User(2L, "李四")
);
Map<Long, String> userMap = users.stream()
    .collect(Collectors.toMap(User::getId, User::getName));
String name = userMap.get(1L); // name為null,需要判空

為了避免這種情況,你可以在映射值時進行非空處理,或者在收集完成后過濾掉值為 null 的鍵值對。

3. 性能問題:并行流中的 “陷阱”

(1)并行流的 “甜蜜陷阱”

Stream 的并行流可以充分利用多核 CPU 的優勢,提高數據處理效率。但在使用 toMap () 時,并行流可能會導致性能問題,甚至數據混亂。

舉個栗子:

List<User> users = generateLargeUserList(10_000_000);
Map<Long, User> userMap = users.parallelStream()
    .collect(Collectors.toMap(User::getId, Function.identity()));

這段代碼在并行流中使用 toMap (),可能會因為線程安全問題導致數據混亂。因為 toMap () 默認使用的是 HashMap,而 HashMap 在多線程環境下是非線程安全的。這時候,你可能會想:“我只是想提高處理效率,難道就這么難嗎?”

(2)解決方案:toConcurrentMap ()

為了解決并行流中的線程安全問題,Java 提供了Collectors.toConcurrentMap()方法。這個方法返回的是 ConcurrentHashMap,支持并發操作,性能更好。

舉個栗子:

Map<Long, User> userMap = users.parallelStream()     .collect(Collectors.toConcurrentMap(User::getId, Function.identity()));

這樣,即使在并行流中使用 toConcurrentMap (),也能保證數據的一致性和線程安全。但需要注意的是,toConcurrentMap () 的性能并不一定比 toMap () 好,具體取決于數據量和并發程度。

二、替代方案:toMap () 的 “平替” 們

既然 toMap () 有這么多坑,那有沒有更好的替代方案呢?答案是肯定的。下面,我將為大家介紹幾種常用的替代方法。

1. groupingBy:分組處理的 “瑞士軍刀”

(1)基本用法:按字段分組

Collectors.groupingBy()是一個非常強大的收集器,它可以將流中的元素按照某個字段進行分組,返回一個 Map,其中鍵是分組字段的值,值是該分組下的元素列表。

舉個栗子:

List<Order> orders = ...;
Map<String, List<Order>> orderMap = orders.stream()
    .collect(Collectors.groupingBy(Order::getUserId));

這樣,orderMap 的鍵是用戶 id,值是該用戶的所有訂單列表。這種方法不僅可以避免重復鍵的問題,還可以方便地進行后續的統計和分析。

(2)進階用法:多級分組

groupingBy () 還支持多級分組,即先按一個字段分組,再按另一個字段分組,返回一個嵌套的 Map。

舉個栗子:

Map<String, Map<String, List<Order>>> orderMap = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getUserId,
        Collectors.groupingBy(Order::getStatus)
    ));

這樣,orderMap 的結構是Map<用戶id, Map<訂單狀態, List<訂單>>>,可以方便地統計每個用戶不同狀態的訂單數量。

(3)統計聚合:與其他收集器結合

groupingBy () 還可以與其他收集器結合使用,進行統計聚合操作。例如,統計每個用戶的訂單總數、總金額等。

舉個栗子:

Map<String, Long> orderCountMap = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getUserId,
        Collectors.counting()
    ));

Map<String, Double> totalAmountMap = orders.stream()
    .collect(Collectors.groupingBy(
        Order::getUserId,
        Collectors.summingDouble(Order::getAmount)
    ));

這樣,orderCountMap 的鍵是用戶 id,值是該用戶的訂單總數;totalAmountMap 的鍵是用戶 id,值是該用戶的訂單總金額。

2. toMap 的安全變種:處理重復鍵和 null 值

(1)處理重復鍵:三參數 toMap ()

前面已經介紹過,toMap () 的三參數重載方法可以自定義合并策略,處理重復鍵的問題。例如,保留舊值、替換新值或合并值。

(2)處理 null 值:過濾或默認值

為了避免鍵或值為 null 的問題,可以在流處理過程中進行過濾,或者在映射時提供默認值。

  • 過濾 null 鍵:
Map<Long, String> userMap = users.stream()
    .filter(user -> user.getId() != null)
    .collect(Collectors.toMap(User::getId, User::getName));
  • 提供默認值:
Map<Long, String> userMap = users.stream()
    .collect(Collectors.toMap(
        User::getId,
        User::getName,
        (oldValue, newValue) -> oldValue,
        () -> new HashMap<>()
    ));

這里,第四個參數() -> new HashMap<>()是一個 Map 的供應商,用于指定返回的 Map 類型。如果不指定,默認返回的是 HashMap。

3. 自定義收集器:靈活應對復雜需求

(1)為什么需要自定義收集器?

雖然 Java 提供的內置收集器已經能夠滿足大多數需求,但在某些情況下,我們可能需要更靈活的收集邏輯。例如,將流中的元素收集到一個自定義的 Map 中,或者在收集過程中進行復雜的轉換和聚合操作。

(2)自定義收集器的實現步驟

自定義收集器需要實現Collector接口,該接口定義了四個方法:supplier()、accumulator()、combiner()和finisher(),以及一個characteristics()方法。

舉個栗子:

public class CustomCollector<T, K, V> implements Collector<T, Map<K, V>, Map<K, V>> {
    privatefinalFunction<T, K> keyMapper;
    privatefinalFunction<T, V> valueMapper;
    privatefinal BinaryOperator<V> mergeFunction;

    public CustomCollector(Function<T, K> keyMapper, Function<T, V> valueMapper, BinaryOperator<V> mergeFunction) {
        this.keyMapper = keyMapper;
        this.valueMapper = valueMapper;
        this.mergeFunction = mergeFunction;
    }

    @Override
    public Supplier<Map<K, V>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<K, V>, T> accumulator() {
        return (map, element) -> {
            K key = keyMapper.apply(element);
            V value = valueMapper.apply(element);
            map.merge(key, value, mergeFunction);
        };
    }

    @Override
    public BinaryOperator<Map<K, V>> combiner() {
        return (map1, map2) -> {
            map2.forEach((key, value) -> map1.merge(key, value, mergeFunction));
            return map1;
        };
    }

    @Override
    publicFunction<Map<K, V>, Map<K, V>> finisher() {
        returnFunction.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
    }
}

這個自定義收集器可以將流中的元素收集到一個 Map 中,支持自定義鍵映射、值映射和合并策略。使用時,可以像這樣調用:

Map<Long, String> userMap = users.stream()
    .collect(new CustomCollector<>(User::getId, User::getName, (oldValue, newValue) -> oldValue));

這樣,就可以避免使用 toMap () 時的重復鍵和 null 值問題,同時保持代碼的靈活性和可讀性。

三、實戰案例:toMap () 的 “坑” 與 “避坑指南”

1. 案例一:用戶行為分析

(1)需求描述

某電商平臺需要分析用戶的購買行為,統計每個用戶的購買次數和總金額。要求將結果存儲到一個 Map 中,其中鍵是用戶 id,值是一個包含購買次數和總金額的對象。

(2)使用 toMap () 的實現

List<Order> orders = ...;
Map<Long, PurchaseStats> purchaseStatsMap = orders.stream()
    .collect(Collectors.toMap(
        Order::getUserId,
        order -> new PurchaseStats(1, order.getAmount()),
        (oldStats, newStats) -> new PurchaseStats(
            oldStats.getCount() + newStats.getCount(),
            oldStats.getTotalAmount() + newStats.getTotalAmount()
        )
    ));

這段代碼使用 toMap () 的三參數重載方法,自定義了合并策略,將每個用戶的購買次數和總金額進行累加。

(3)問題分析

  • 重復鍵處理:如果同一個用戶有多個訂單,合并策略會正確累加購買次數和總金額。
  • null 值處理:如果某個訂單的用戶 id 為 null,會拋出NullPointerException。
  • 性能問題:如果訂單量很大,并行流可能會導致性能問題。

(4)優化方案

  • 過濾 null 用戶 id:
Map<Long, PurchaseStats> purchaseStatsMap = orders.stream()
    .filter(order -> order.getUserId() != null)
    .collect(Collectors.toMap(
        Order::getUserId,
        order -> new PurchaseStats(1, order.getAmount()),
        (oldStats, newStats) -> new PurchaseStats(
            oldStats.getCount() + newStats.getCount(),
            oldStats.getTotalAmount() + newStats.getTotalAmount()
        )
    ));
  • 使用并行流和 toConcurrentMap ():
Map<Long, PurchaseStats> purchaseStatsMap = orders.parallelStream()
    .filter(order -> order.getUserId() != null)
    .collect(Collectors.toConcurrentMap(
        Order::getUserId,
        order -> new PurchaseStats(1, order.getAmount()),
        (oldStats, newStats) -> new PurchaseStats(
            oldStats.getCount() + newStats.getCount(),
            oldStats.getTotalAmount() + newStats.getTotalAmount()
        )
    ));

這樣,可以提高處理效率,同時避免線程安全問題。

2. 案例二:日志分析

(1)需求描述

某系統需要分析日志數據,統計每個日志級別(如 INFO、WARN、ERROR)的日志數量,并將結果存儲到一個 Map 中,其中鍵是日志級別,值是日志數量。

(2)使用 groupingBy 的實現

List<Log> logs = ...;
Map<String, Long> logCountMap = logs.stream()
    .collect(Collectors.groupingBy(
        Log::getLevel,
        Collectors.counting()
    ));

這段代碼使用 groupingBy () 和 counting () 收集器,簡單高效地統計了每個日志級別的日志數量。

(3)問題分析

  • 無需處理重復鍵:因為日志級別是唯一的,所以不會出現重復鍵的問題。
  • 性能問題:如果日志量很大,并行流可以提高處理效率。

(4)優化方案

Map<String, Long> logCountMap = logs.parallelStream()
    .collect(Collectors.groupingByConcurrent(
        Log::getLevel,
        Collectors.counting()
    ));

使用groupingByConcurrent()可以在并行流中高效地進行分組統計,提高處理效率。

四、總結:toMap () 的正確打開方式

1. 什么時候可以用 toMap ()?

  • 數據明確唯一:當你確定流中的元素不會產生重復鍵時,可以使用 toMap ()。
  • 簡單映射需求:當你只需要將元素映射到 Map 中,不需要復雜的合并策略或統計聚合時,可以使用 toMap ()。
  • 非并行處理:當你不需要使用并行流時,可以使用 toMap ()。

2. 什么時候應該避免使用 toMap ()?

  • 可能存在重復鍵:如果流中的元素可能產生重復鍵,應該使用三參數的 toMap () 或其他替代方法。
  • 需要處理 null 值:如果鍵或值可能為 null,應該在流處理過程中進行過濾或提供默認值。
  • 并行處理需求:如果需要使用并行流,應該使用 toConcurrentMap () 或其他支持并發的收集器。

3. 替代方案推薦

  • 分組處理:使用 groupingBy () 進行分組統計,避免重復鍵和 null 值問題。
  • 自定義收集器:當內置收集器無法滿足需求時,使用自定義收集器實現靈活的收集邏輯。
  • 并行處理:使用 toConcurrentMap () 或 groupingByConcurrent () 在并行流中進行高效處理。

4. 最后的忠告

toMap () 是一個強大的工具,但也是一個危險的工具。它的簡單易用性可能會掩蓋潛在的問題,導致代碼在生產環境中出現意想不到的錯誤。因此,在使用 toMap () 時,一定要謹慎處理重復鍵、null 值和性能問題,或者選擇更合適的替代方案。

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2024-07-10 10:15:43

2024-11-05 10:24:50

2022-03-26 08:49:13

MySQL數據存儲

2018-02-06 08:42:10

永久內存XPoint閃存

2021-01-29 11:05:50

PrintPython代碼

2016-06-12 09:48:40

2016-05-03 09:48:58

2017-08-31 17:00:20

2023-11-29 08:19:45

Go泛型缺陷

2020-09-08 08:45:39

jupyter插件代碼

2021-09-10 08:00:00

Python機器學習開發

2021-03-17 16:53:51

IO多路

2017-12-07 11:27:30

編程開發代碼

2018-04-10 13:40:14

Kubernetes容器服務器

2023-10-31 08:01:48

Mybatis參數jdbcurl?

2025-04-09 03:00:00

簽字板代碼canvas

2021-02-07 10:17:22

項目架構技術管理

2021-04-07 20:01:23

Go變量常量

2021-05-21 14:26:18

ObjectMap前端

2017-07-13 12:33:15

戴爾
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 亚洲精品一区二区三区四区高清 | 国产精品一区二区在线观看 | 夫妻午夜影院 | 99久久婷婷国产综合精品电影 | 久综合| 性色视频| 久久九精品 | 精品亚洲视频在线 | 国产激情视频在线观看 | 日韩综合网 | 亚洲精品一区二区三区 | 五月婷六月丁香 | 亚洲欧美激情精品一区二区 | 欧美乱人伦视频 | 国产成人综合亚洲欧美94在线 | 91精品国产一区二区在线观看 | 免费一区 | 国产乱码精品一区二区三区忘忧草 | 欧美一区二区在线 | 精品日韩电影 | 男人天堂久久久 | 亚洲综合在线一区二区 | 午夜一区二区三区在线观看 | 欧美高清视频一区 | 国产一区二区三区在线观看免费 | 亚洲成人av | 日本aa毛片a级毛片免费观看 | 日韩欧美精品在线 | 中文字幕福利视频 | 国产精品69av | 日韩一区中文字幕 | 成人精品一区二区三区中文字幕 | 九九综合九九 | 国产中文一区二区三区 | 日韩国产一区二区三区 | 一区日韩 | 国产精品久久久亚洲 | 日韩喷潮 | 国产一区h| 国产在线高清 | 一级二级三级黄色 |