中招了,重寫TreeMap的比較器引發的問題…


需求背景

  • 給一個無序的map,按照value的值進行排序,value值越小,排在越前面。
  • key和value都不為null
  • value可能相同
  • 返回結果為一個相同的有序map

代碼如下所示:

1 // 假設,key=商品id,value=商品剩余庫存
2 Map<Long, Integer> map = new HashMap<>();
3 map.put(1L, 10);
4 map.put(2L, 20);
5 map.put(3L, 10);

到這里,大家可以先想想,如果是你會怎么解決?

我的解決思路

1、使用TreeMap,因為TreeMap可以對元素進行排序

2、重寫TreeMap的比較器

代碼如下所示:

 1 // 承接上面的代碼
 2 // 按照 value 排序
 3 Map<Long, Integer> treeMap1 = new TreeMap<>(new Comparator<Long>() {
 4     @Override
 5     public int compare(Long o1, Long o2) {
 6         // 1、如果v1等於v2,則值為0
 7         // 2、如果v1小於v2,則值為-1
 8         // 3、如果v1等於v2,則值為1
 9         Integer value1 = map.get(o1);
10         Integer value2 = map.get(o2);
11         return value1.compareTo(value2);
12     }
13 });
14 treeMap1.putAll(map);
15 System.out.println(treeMap1);

運行后的結果為:

{1=10, 2=20}

what?為什么我們添加了3個元素,結果少了一個呢?

中招了,重寫TreeMap的比較器引發的問題...

TreeMap putAll源碼分析

讓我們來看看 putAll 的具體過程

1、分析 TreeMap.putAll

源碼如下所示:

 1 public void putAll(Map<? extends K, ? extends V> map) {
 2     // 一、獲取待添加的map的大小
 3     int mapSize = map.size();
 4     // 二、當前的size大小等於0 且 待添加的map的大小不等於0 且 待添加的map是SortedMap的實現類,則執行以下邏輯
 5     if (size==0 && mapSize!=0 && map instanceof SortedMap) {
 6         // 1、獲取待添加的map的比較器
 7         Comparator<?> c = ((SortedMap<?,?>)map).comparator();
 8         // 2、如果兩個比較器相同,則執行以下邏輯
 9         if (c == comparator || (c != null && c.equals(comparator))) {
10             // 3、修改次數+1
11             ++modCount;
12             try {
13                 // 4、基於排序數據的線性時間樹構建算法,進行build
14                 buildFromSorted(mapSize, map.entrySet().iterator(),
15                         null, null);
16             } catch (java.io.IOException cannotHappen) {
17             } catch (ClassNotFoundException cannotHappen) {
18             }
19             return;
20         }
21     }
22     // 三、如果不符合上面的條件,則執行父類的 putAll 方法
23     super.putAll(map);
24 }

從上面源碼,不難看出,我們的數據符合 流程二,但是不符合 流程二-2,所以我們會執行父類的 putAll 方法,即流程三。

2、分析 AbstractMap.putAll

TreeMap 繼承 AbstractMap,所以 super.putAll(map),執行的 putAll 為 AbstractMap 的 putAll 方法,源碼如下所示:

1 public void putAll(Map<? extends K, ? extends V> m) {
2     // 遍歷 m map,將它所有的值,使用put方法,全部添加到當前的map中
3     for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
4         put(e.getKey(), e.getValue());
5 }

這段代碼簡單,就是一個遍歷添加元素的。

但是有一個問題,這里的 put 方法執行的是誰的 put 方法呢?

  • 1、AbstractMap.put
  • 2、TreeMap.put

這里大家可以先思考1分鍾,然后再繼續往下看。

答案是:

執行的是 TreeMap.put

回答錯誤 or 不知道真實原因的小伙伴,可以去網上搜搜答案,這里是一個很重要的基礎知識點哦。

3、分析 TreeMap.put

源代碼如下所示:

 1 public V put(K key, V value) {
 2     // 一、獲取根節點
 3     TreeMap.Entry<K,V> t = root;
 4     // 二、判斷跟節點是否為空
 5     if (t == null) {
 6         // 類型檢查 and null 檢查
 7         compare(key, key); // type (and possibly null) check
 8         // 創建根節點
 9         root = new TreeMap.Entry<>(key, value, null);
10         size = 1;
11         // 修改次數加1
12         modCount++;
13         return null;
14     }
15 
16     int cmp;
17     TreeMap.Entry<K,V> parent;
18     // 獲取比較器
19     Comparator<? super K> cpr = comparator;
20     // 三、如果比較器不為空,則執行一下邏輯,即自定義比較器執行邏輯
21     if (cpr != null) {
22         do {
23             // 1、將t節點賦值給parent
24             parent = t;
25             // 2、比較t節點的key是否與待添加的key相等
26             cmp = cpr.compare(key, t.key);
27             // 3、如果返回值小於0,則將左子樹賦值給t節點,即后續遍歷左子樹
28             if (cmp < 0)
29                 t = t.left;
30             // 4、如果返回值大於0,則將右子樹賦值給t節點,即后續遍歷右子樹
31             else if (cmp > 0)
32                 t = t.right;
33             else
34                 // 5、如果返回值為0,則覆蓋原來的值
35                 return t.setValue(value);
36         } while (t != null);
37     }
38     // 四、如果比較器為空,則執行以下邏輯,即默認執行邏輯
39     else {
40       // 這部分邏輯,先忽略
41     }
42     TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
43     if (cmp < 0)
44         parent.left = e;
45     else
46         parent.right = e;
47     fixAfterInsertion(e);
48     size++;
49     modCount++;
50     return null;
51 }

我們結合上面的源碼和我們自定義的排序器,就可以發現以下問題:

1、我們比較的是兩個 value 的大小,而 value 可能是一樣的。

中招了,重寫TreeMap的比較器引發的問題...

這種情況下,就會覆蓋原來的值,這個就是我們執行 putAll 后,元素缺失的原因了。

中招了,重寫TreeMap的比較器引發的問題...

好了既然問題找到了,那如何解決這個問題呢?

如果是你,你會怎么解決呢?可以花一分鍾時間思考一下,再看后面的內容。

4、解決 TreeMap.putAll,元素缺失的問題

我當時想到最直接的方案就是,在 value 相等的情況下,不返回 0,返回1 or -1,這樣就可以最簡單、最快捷的解決這個問題了。

修改后的代碼如下所示:

 1 // 這里換了一種寫法,是java8的特性,簡化了代碼(為了偷懶)
 2 Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {
 3     // 1、如果v1等於v2,則值為0
 4     // 2、如果v1小於v2,則值為-1
 5     // 3、如果v1等於v2,則值為1
 6     Integer value1 = map.get(key1);
 7     Integer value2 = map.get(key2);
 8 
 9     int result = value1.compareTo(value2);
10 
11     if (result == 0) {
12         return -1;
13     }
14     return result;
15 });
16 
17 treeMap2.putAll(map);
18 System.out.println(treeMap2);

運行后的結果為:

{3=10, 1=10, 2=20}

我們可以發現,3個值都有了,並且是有序的,完美符合需求!好了,關機下班!

中招了,重寫TreeMap的比較器引發的問題...

然而事情並沒有結束 (大家可以想一下,這樣寫會有什么問題呢?)

新的問題出現

第二天,高高興興的寫着業務代碼、調試邏輯,突然一個 空指針 的報錯,出現了。這也太常見了吧,3分鍾內解決!

中招了,重寫TreeMap的比較器引發的問題...

排查了半天,發現又回到了昨天的修改的那段邏輯了。

1、TreeMap.get 獲取不到值

簡化版代碼如下所示:

 1 // 假設,key=商品id,value=商品剩余庫存
 2 Map<Long, Integer> map = new HashMap<>();
 3 map.put(1L, 10);
 4 map.put(2L, 20);
 5 map.put(3L, 10);
 6 
 7 // 排序
 8 Map<Long, Integer> treeMap2 = new TreeMap<>((key1, key2) -> {
 9     Integer value1 = map.get(key1);
10     Integer value2 = map.get(key2);
11 
12     int result = value1.compareTo(value2);
13 
14     if (result == 0) {
15         return -1;
16     }
17     return result;
18 });
19 treeMap2.putAll(map);
20 System.out.println(treeMap2);
21 
22 // 獲取商品1的剩余數量
23 Integer quantity = treeMap2.get(1L);
24 System.out.println(quantity);

運行后的結果為:

{3=10, 1=10, 2=20}
null

這個結果令我百思不得其解,只能看看源碼咯。

2、分析 TreeMap.get

源碼如下所示:

 1 public V get(Object key) {
 2     // 根據key獲取節點
 3     TreeMap.Entry<K,V> p = getEntry(key);
 4     // 節點為空則返回null,否則返回節點的 value 值
 5     return (p==null ? null : p.value);
 6 }
 7 
 8 final TreeMap.Entry<K,V> getEntry(Object key) {
 9     // 一、如果比較器不為空,則執行一下邏輯
10     if (comparator != null)
11         // 1、使用自定義比較器取出key對應的節點
12         return getEntryUsingComparator(key);
13     // 二、如果比較器為空,且key為null,則拋空指針異常
14     if (key == null)
15         throw new NullPointerException();
16     @SuppressWarnings("unchecked")
17     Comparable<? super K> k = (Comparable<? super K>) key;
18     TreeMap.Entry<K,V> p = root;
19     // 三、取出key對應的節點
20     while (p != null) {
21         int cmp = k.compareTo(p.key);
22         if (cmp < 0)
23             p = p.left;
24         else if (cmp > 0)
25             p = p.right;
26         else
27             return p;
28     }
29     return null;
30 }

從上面的源碼,我們可以發現,問題肯定就是出現在 getEntryUsingComparator 方法里了。

2、分析 TreeMap.getEntryUsingComparator

源碼如下所示:

 1 final TreeMap.Entry<K,V> getEntryUsingComparator(Object key) {
 2     // 一、將key轉換成對應的類型
 3     @SuppressWarnings("unchecked")
 4     K k = (K) key;
 5     // 二、獲取比較器
 6     Comparator<? super K> cpr = comparator;
 7     // 三、判斷比較器是否為空
 8     if (cpr != null) {
 9         // 1、遍歷map,取出key對應的節點對象
10         TreeMap.Entry<K,V> p = root;
11         while (p != null) {
12             int cmp = cpr.compare(k, p.key);
13             // 2、如果小於0,則將左節點的值賦值給p
14             if (cmp < 0)
15                 p = p.left;
16             // 3、如果大於0,則將右節點的值賦值給p
17             else if (cmp > 0)
18                 p = p.right;
19             else
20                 // 4、如果等於0,則返回p節點
21                 return p;
22         }
23     }
24     return null;
25 }

結合上面的源碼,和我們之前自定義的比較器,我們不難發現問題出現在哪里:

中招了,重寫TreeMap的比較器引發的問題...

自定義比較器,沒有返回0的情況

問題找到了,解決吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM