需求背景
- 給一個無序的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 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 可能是一樣的。
這種情況下,就會覆蓋原來的值,這個就是我們執行 putAll 后,元素缺失的原因了。
好了既然問題找到了,那如何解決這個問題呢?
如果是你,你會怎么解決呢?可以花一分鍾時間思考一下,再看后面的內容。
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個值都有了,並且是有序的,完美符合需求!好了,關機下班!
然而事情並沒有結束 (大家可以想一下,這樣寫會有什么問題呢?)!
新的問題出現
第二天,高高興興的寫着業務代碼、調試邏輯,突然一個 空指針 的報錯,出現了。這也太常見了吧,3分鍾內解決!
排查了半天,發現又回到了昨天的修改的那段邏輯了。
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 }
結合上面的源碼,和我們之前自定義的比較器,我們不難發現問題出現在哪里:
自定義比較器,沒有返回0的情況
問題找到了,解決吧!