源碼閱讀(21):Java中其它主要的Map結構——TreeMap容器(1)

(接上文《源碼閱讀(20):Java中主要的Map結構——HashMap容器(下2)》)

我們通過解讀Java自帶的各種Map容器既可以瞭解到,構成這些原生容器的數據組織結構基本上就只有三種:數組、鏈表和紅黑樹。也就是說如果讀者想徹底瞭解Java自帶的Map容器的工作細節,就必須首先詳細理解這三類數據結構,然後在這個基礎上再進行“知識移植”即可。

例如Java自帶的Map容器中,有一個叫做TreeMap的容器,後者就是主要基於紅黑樹構建的一種Map容器,其重要的工作過程和HashMap中基於紅黑樹工作的部分類似。從本部分開始,我們將對Java自帶的除HashMap以外的多種Map容器進行介紹。

1、TreeMap概述和基本使用

TreeMap容器基於紅黑樹進行構建,其容器內所有的K-V鍵值對對象都是這個紅黑樹上的一個節點。至於這些對象的排列順序如何決定,主要是基於兩種邏輯。第一種邏輯是基於K-V鍵值對中Key信息的Hash值來決定,第二種是基於使用者設定的java.util.Comparator接口的實現來決定,以上兩種排列順序邏輯的選擇完全取決於TreeMap容器實例化時使用的構造函數。

由於內部是紅黑樹結構的原因,TreeMap容器擁有較好的時間複雜度,進行節點查詢、添加、移除操作時平均時間複雜度可控制在O(logn)。另外在TreeMap容器類的官方介紹中,有這麼一句話:

Algorithms are adaptations of those in Cormen, Leiserson, and Rivest’s Introduction to Algorithms.

這本書的音譯是《算法導論》,目前這本書已經發行第四版,全國各大書店/網上書店均有銷售。這本書最初初版的時間爲2004年,是一本非常經典的計算機算法書籍,建議各位讀者有時間的時候可以閱讀。
在這裏插入圖片描述
最後請注意,TreeMap容器並非線程安全的容器,且在Java原生的線程安全的容器中並沒有類似內部結構的容器可供選擇。所以如果使用者需要在線程安全的場景下使用TreeMap容器,可以採用如下方式將一個線程不安全的容器封裝爲一個線程安全的容器:

// ......
TreeMap<String, Object> currentMap = new TreeMap<>();
// 封裝成線程安全的Map容器
Map<String, Object> cMap = Collections.synchronizedMap(currentMap);
// ......

以上代碼很簡單無需過多說明,主要是Collections工具類的synchronizedMap方法的內部代碼如下:

// ......
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
  return new SynchronizedMap<>(m);
}

/**
 * @serial include
 */
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
  // 被SynchronizedMap類代理的真實Map對象
  // Backing Map
  private final Map<K,V> m;
  // 用來加同步鎖的對象
  // Object on which to synchronize
  final Object mutex;
  SynchronizedMap(Map<K,V> m) {
    this.m = Objects.requireNonNull(m);
    mutex = this;
  }
  SynchronizedMap(Map<K,V> m, Object mutex) {
  	this.m = m;
	this.mutex = mutex;
  }
  public int size() {
    synchronized (mutex) {return m.size();}
  }
  public boolean isEmpty() {
    synchronized (mutex) {return m.isEmpty();}
  }
  //......
  // 省略了一些代碼
  //......
  public V remove(Object key) {
	synchronized (mutex) {return m.remove(key);}
  }
  public void clear() {
    synchronized (mutex) {m.clear();}
  }
  
  //......
  // 省略了一些代碼
  //......
  	
  @Override
  public void forEach(BiConsumer<? super K, ? super V> action) {
  	synchronized (mutex) {m.forEach(action);}
  }
  @Override
  public V replace(K key, V value) {
	synchronized (mutex) {return m.replace(key, value);}
  }	
  //......
  // 省略了一些代碼
  //......
}
// ......

雖然Java中有這種SynchronizedMap代理類,可以將一個線程不安全的容器封裝爲一個線程安全的容器,但是由於SynchronizedMap內部使用的是悲觀鎖機制的實現,所以推薦在較高併發的場景下還是優先選擇使用java.util.concurrent包下的相關數據結構類。下圖展示了TreeMap容器類的主要繼承體系:
在這裏插入圖片描述

2、TreeMap容器中的典型方法

由於之前的文章已經介紹過紅黑樹的特性了(可參見文章《源碼閱讀(17):紅黑樹在Java中的實現和應用》),所以本小節就直接介紹TreeMap容器中的幾個典型操作方法,順便和讀者一起復習紅黑樹的工作特點。在介紹這些方法前,我們首先給出TreeMap容器中重要的變量信息:

public class TreeMap<K,V> extends AbstractMap<K,V> 
  implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
  // ......
  // 這個比較器非常重要,它記錄了紅黑樹中各節點排列順序的判定邏輯;
  // 該比較器對象可以爲null,如果爲null的情況那麼在判定紅黑樹節點排列順序時,
  // 將採用TreeMap容器原生的基於K-V鍵值對Key-Hash值的判定方式。
  private final Comparator<? super K> comparator;
  // 該變量記錄當前TreeMap容器中紅黑樹的根節點
  private transient Entry<K,V> root;
  // 該變量記錄當前TreeMap容器中的K-V鍵值對對象數量
  private transient int size = 0;
  // modCount變量記錄當前TreeMap容器執行“寫”操作的次數
  private transient int modCount = 0;
  // ......
}

2.1、TreeMap構造方式

TreeMap容器中一共有四個構造函數,這四個構造函數實際上都在完成同一個工作,即根據調用情況決定comparator變量的賦值情況,以及TreeMap容器初始時的紅黑樹結構狀態。

public class TreeMap<K,V> extends AbstractMap<K,V> 
  implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
  // ......
  // 該默認的構造函數,將設定TreeMap容器中的comparator比較器爲null
  // 基於上文的介紹我們就知道,這樣實例化的TreeMap容器對象將採用K-V鍵值對自身Key-Hash值完成排序比較
  public TreeMap() {
    comparator = null;
  }
  // 該構造函數將爲當前TreeMap容器對象設定一個比較器
  public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
  }
  // 該構造函數將一個K-V鍵值對容器的所有對象設定到新的TreeMap容器中
  // 並且由於原容器沒有實現SortedMap接口,所以設定當前TreeMap容器的comparator比較器爲null
  public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
  }
  // 該構造函數將一個實現了SortedMap接口的K-V鍵值對容器的所有對象設定到新的TreeMap容器中
  // 並且由於原容器實現了SortedMap接口,所以將原SortedMap容器使用的comparator比較器設定到當前容器
  public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
      buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
      
    } catch (ClassNotFoundException cannotHappen) {
      
    }
  }
  // ......
}

請注意以上代碼片段中的buildFromSorted方法,這個方法的主要意義是基於一個有序的數據集合(可能是某個有序的Map容器中基於Map.Entries的迭代器,也可能是有序Map容器中基於K-V鍵值對中所有Key信息的迭代器等等)構建一顆紅黑樹樹。這個方法在TreeMap容器的方法定義中有兩個重載的方法,其中的代碼內容如下所示:

// ......
// size參數、it參數和defaultVal參數的含義將在後續的代碼閱讀內容中進行說明
private void buildFromSorted(int size, Iterator<?> it, ObjectInputStream str, V defaultVal) {
  this.size = size;
  // 遞歸進行處理
  root = buildFromSorted(0, 0, size-1, computeRedLevel(size), it, str, defaultVal);
}
// 該方法中level:表示當前正在構建的滿樹深度
// lo:表示當前子樹的第一個節點索引位,第一次遞歸時從0號索引位開始
// hi:表示當前子樹的最後一個節點索引位,第一次遞歸時從size-1號索引位開始
// redLevel:表示紅黑樹中紅節點的起始深度
private final Entry<K,V> buildFromSorted(int level, int lo, int hi, int redLevel,
                                             Iterator<?> it, ObjectInputStream str, V defaultVal) {
  // 如果條件成立,說明滿二叉樹的構造完成,返回null
  if (hi < lo) 
    return null;
  // 找到本次遍歷集合的中間索引位,代碼很好理解:無符號右移一位既是“除以2”操作。
  int mid = (lo + hi) >>> 1;
  Entry<K,V> left  = null;
  // 如果當前子樹的最小索引位小於當前確定的中間索引位,則繼續構建下一級子樹(以當前mid索引位爲根節點的左子樹)
  // 下一級左子樹構造時,指定的滿二叉樹深度 + 1,子樹的起始索引位爲0,子樹的結束索引位爲mid-1。
  if (lo < mid)
    left = buildFromSorted(level+1, lo, mid - 1, redLevel, it, str, defaultVal);

  // extract key and/or value from iterator or stream
  K key;
  V value;
  // 以上代碼我們只是確定了子樹的索引定位,還沒有真正開始將集合構建滿二叉樹
  // 所以這裏開始進行滿二叉樹的構建:這裏一共有四種可能的場景
  // 當it != null,defaultVal == null:以Map.Entry的形式取得對象,構建本次紅黑樹的節點
  // 當it != null,defaultVal != null:以K-V鍵值對的形式取得對象,構建本次紅黑樹的節點,且key的值來源於it迭代器,value值默認爲defaultVal
  // 當it == null,defaultVal == null:以對象反序列化的形式取得對象,構建本次紅黑樹的節點,key值來源於str反序列化讀取的對象信息,value值也來源於str反序列化讀取的對象信息
  // 當it == null,defaultVal != null:以對象反序列化的形式取得對象,構建本次紅黑樹的節點,key值來源於str反序列化讀取的對象信息,但是Value值默認爲defaultVal
  if (it != null) {
    if (defaultVal==null) {
      Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
      key = (K)entry.getKey();
      value = (V)entry.getValue();
    } else {
      key = (K)it.next();
      value = defaultVal;
    }
  } else { // use stream
    key = (K) str.readObject();
    value = (defaultVal != null ? defaultVal : (V) str.readObject());
  }
  Entry<K,V> middle =  new Entry<>(key, value, null);

  // color nodes in non-full bottommost level red
  // 如果當前正在構建的滿二叉樹的深度剛好是開始前計算出的紅色節點的深度
  // 則將本次構建的middle節點的顏色標紅
  if (level == redLevel)
    middle.color = RED;
  
  // 如果當前節點的左子樹不爲null,則將當前節點和它的左子樹進行關聯
  if (left != null) {
    middle.left = left;
    left.parent = middle;
  }
  
  // 如果之前計算得到的當前節點子樹的結束索引位大於計算得到的中間索引位
  // 則進行當前middle節點的右子樹構建
  if (mid < hi) {
    Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel, it, str, defaultVal);
    middle.right = right;
    right.parent = middle;
  }
  return middle;
}
// ......

應該注意的是以上代碼第一個最關鍵的步驟,並不是讀取當前正在處理結點的K值和V值,並形成一個新的TreeMap.Entry對象,而是通過遞歸的方式找到需要構建的紅黑樹的第一個節點索引位,相關的代碼內容可以用下圖進行描述:
在這裏插入圖片描述
遞歸方法中構建的紅黑樹中,第一個被初始化的TreeMap.Entry對象的位置,是索引號爲0的節點位置;第二個被初始化的TreeMap.Entry對象的位置,是索引號爲1的節點位置,它將作爲第一個節點的右子樹存在。

從圖中可以看出每次遞歸進行紅黑樹構造時,都是以當前計算得到的mid索引爲的節點作爲根節點來進行其左子樹和右子樹的構建。而當前節點的左子樹可能是沒有的,但是其右子樹一定會有節點,如下圖所示:
在這裏插入圖片描述
當構造指定索引位上紅黑樹的節點時,每一個節點都是一個TreeMap.Entry對象,這個新對象中Key信息的來源和Value信息的來源根據size參數、it參數、str參數、defaultVal參數的傳值效果有所區別

  • 當it != null,defaultVal == null:那麼說明構造TreeMap.Entry對象是參考另一個TreeMap.Entry對象。
  • 當it != null,defaultVal != null:那麼說明構造TreeMap.Entry對象時,key值來源於it迭代器,value值默認爲defaultVal。
  • 當it == null,defaultVal == null:說明構造TreeMap.Entry對象時,key值來源於str反序列化讀取的對象信息,value值也來源於str反序列化讀取的對象信息
  • 當it == null,defaultVal != null:以對象反序列化的形式取得對象,構建本次滿二叉樹的節點,key值來源於str反序列化讀取的對象信息,但是value值默認爲defaultVal

2.2、TreeMap中的批量添加操作

我們可以使用TreeMap容器提供的putAll(Map<? extends K, ? extends V> map)方法批量添加K-V鍵值對數據,根據當前TreeMap容器已有的K-V鍵值對的數量情況,添加步驟又不一樣。

public void putAll(Map<? extends K, ? extends V> map) {
  int mapSize = map.size();
  // 如果當前TreeMap容器中K-V鍵值對數量爲0,並且將要添加的K-V鍵值對數量不爲0
  // 並且當前傳入的map容器實現了SortedMap接口(說明是有序的Map容器)
  if (size==0 && mapSize!=0 && map instanceof SortedMap) {
    // 取得傳入的有序map容器的comparator比較器對象(記爲對象c)
    Comparator<?> c = ((SortedMap<?,?>)map).comparator();
    // 如果比較器對象c,和當前TreeMap容器使用的比較器是同一對象
    // 則使用上文中已將介紹的buildFromSorted方法構建一顆新的紅黑樹
    // 這也就意味着之前treemap容器中已有的K-V鍵值對將不再進行維護——但好在之前treemap容器中並沒有K-V鍵值對信息。
    if (c == comparator || (c != null && c.equals(comparator))) {
      ++modCount;
      try {
        buildFromSorted(mapSize, map.entrySet().iterator(), null, null);
      } catch (java.io.IOException cannotHappen) {
      } catch (ClassNotFoundException cannotHappen) {
      }
      return;
    }
  }
  // 如果當前TreeMap容器的狀態不能使以上兩個嵌套的if條件成立
  // 則對當前批量添加的K-V鍵值對信息,逐一進行操作
  super.putAll(map);
}

============
(接下文《源碼閱讀(22):Java中其它主要的Map結構——TreeMap容器(2)》)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章