JDK容器學(xué)習(xí)之HashMap (二) : 讀寫邏輯詳解

Map讀寫實現(xiàn)邏輯說明

前一篇博文 JDK容器學(xué)習(xí)之HashMap (一) : 底層存儲結(jié)構(gòu)分析 分析了HashMap的底層存儲數(shù)據(jù)結(jié)構(gòu)

通過put(k,v)方法的分析,說明了為什么Map底層用數(shù)組進(jìn)行存儲,為什么Node內(nèi)部有一個next節(jié)點(diǎn)骇扇,這篇則將集中在讀寫方法的具體實現(xiàn)上

本片博文將關(guān)注的重點(diǎn):

  • 通過key獲取value的實現(xiàn)邏輯
  • 新增一個kv對的實現(xiàn)邏輯
  • table 數(shù)組如何自動擴(kuò)容
  • 如何刪除一個kv對(刪除kv對之后欢伏,數(shù)組長度是否會縮水 ?)

1. 根據(jù)key索引

get(key) 作為map最常用的方法之一歌焦,根據(jù)key獲取映射表中的value淮捆,通常時間復(fù)雜度為o(1)

在分析之前,有必要再把HashMap的數(shù)據(jù)結(jié)構(gòu)撈出來看一下

結(jié)構(gòu)描述

根據(jù)上面的結(jié)構(gòu)憋飞,如果讓我們自己來實現(xiàn)這個功能霎苗,對應(yīng)的邏輯應(yīng)該如下:

  • 計算key的hash值
  • 根據(jù)hash確定在table 數(shù)組中的位置
  • 判斷數(shù)組的Node對象中key是否等同與傳入的key
  • 若不是,則一次掃描next節(jié)點(diǎn)的key榛做,直到找到為止

jdk實現(xiàn)如下

public V get(Object key) {
   Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  // 判斷條件 if 內(nèi)部邏輯如下
  // table 數(shù)組已經(jīng)初始化(即非null唁盏,長度大于0)
  // 數(shù)組中根據(jù)key查到的Node對象非空
  if ((tab = table) != null && (n = tab.length) > 0 &&
      (first = tab[(n - 1) & hash]) != null) {
      if (first.hash == hash && // always check first node
          ((k = first.key) == key 
          || (key != null && key.equals(k))))
          return first;
      if ((e = first.next) != null) {
          if (first instanceof TreeNode)
              // 紅黑樹中查找
              return ((TreeNode<K,V>)first).getTreeNode(hash, key);
          do {// 遍歷鏈表
              if (e.hash == hash &&
                  ((k = e.key) == key 
                  || (key != null && key.equals(k))))
                  return e;
          } while ((e = e.next) != null);
      }
  }
  return null;
}

上面的邏輯算是比較清晰,再簡單的劃一下重點(diǎn)

  1. 通過key定位table數(shù)組中索引的具體邏輯
  • hash(key) & (table.length - 1)
  • key的hash值與(數(shù)組長度-1)進(jìn)行按位與检眯,計算得到下標(biāo)
  1. 判斷Node是否為所查找的目標(biāo)邏輯
  • node.hash == hash(key) && (node.key == key || (key!=null && key.equals(node.key))
  • 首先是hash值必須相等
  • and == or equals
    • key為同一個對象
    • or Node的key等價于傳入的key
  1. TreeNode 是個什么鬼

上面的邏輯中厘擂,當(dāng)出現(xiàn)hash碰撞時,會判斷數(shù)組中的Node對象是否為 TreeNode锰瘸,如果是則調(diào)用 TreeNode.getTreeNode(hash,key) 方法

那么這個TreeNode有什么特殊的地方呢刽严?

2. TreeNode 分析

TreeNode 依然是 HashMap 的內(nèi)部類, 不同于Node的是,它繼承自LinkedHashMap.Entry避凝,相比較與Node對象而言舞萄,多了兩個屬性 before, after

1. 數(shù)據(jù)結(jié)構(gòu)

TreeNode對象中,包含的數(shù)據(jù)如下(將父類中的字段都集中在下面了)

// Node 中定義的屬性
final int hash;
final K key;
V value;
Node<K,V> next;
// ---------------

// LinkedHashMap.Entry 中的屬性
Entry<K,V> before, after;
// ---------------


// TreeNode 中定義的屬性
TreeNode<K,V> parent;  // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;    // needed to unlink next upon deletion
boolean red;

2. 內(nèi)部方法

方法比較多管削,實現(xiàn)也不少倒脓,但是看看方法名以及注釋,很容易猜到這是個什么東西了

紅黑樹

具體方法實現(xiàn)身略(對紅黑樹實現(xiàn)有興趣的含思,就可以到這里來膜拜教科書的實現(xiàn)方式)

3. TreeNode 方式的HashMap存儲結(jié)構(gòu)

普通的Node就是一個單向鏈表崎弃,因此HashMap的結(jié)構(gòu)就是上面哪種

TreeNode是一顆紅黑樹的結(jié)構(gòu),所以對上面的圖走一下簡單的改造含潘,將單向鏈表改成紅黑樹即可

newTech

3. 添加kv對

博文 JDK容器學(xué)習(xí)之HashMap (一) : 底層存儲結(jié)構(gòu)分析 對于添加kv對的邏輯進(jìn)行了說明饲做,因此這里將主要集中在數(shù)組的擴(kuò)容上

擴(kuò)容的條件: 默認(rèn)擴(kuò)容加載因子為(0.75),臨界點(diǎn)在當(dāng)HashMap中元素的數(shù)量等于table數(shù)組長度加載因子,長度擴(kuò)為原來的2倍*

數(shù)組擴(kuò)容方法, 實現(xiàn)比較復(fù)雜遏弱,先擼一把代碼盆均,并加上必要注釋

final Node<K,V>[] resize() {
  Node<K,V>[] oldTab = table; // 持有舊數(shù)組的一份引用
  int oldCap = (oldTab == null) ? 0 : oldTab.length;
  int oldThr = threshold;
  int newCap, newThr = 0;
  if (oldCap > 0) {
      if (oldCap >= MAXIMUM_CAPACITY) { 
          // 容量超過上限,直接返回漱逸,不用再繼續(xù)分配
          threshold = Integer.MAX_VALUE;
          return oldTab;
      }
      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
               oldCap >= DEFAULT_INITIAL_CAPACITY)
          // 新的數(shù)組長度設(shè)置為原數(shù)組長度的兩倍
          // 新的閥值為舊閥值的兩倍缀踪,
          newThr = oldThr << 1; // double threshold
  } else if (oldThr > 0) {
      // initial capacity was placed in threshold
      newCap = oldThr;
  } else { 
      // 首次初始化
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  }
  
  
  if (newThr == 0) { 
      float ft = (float) newCap * loadFactor;
      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
  }
  threshold = newThr;
  @SuppressWarnings({"rawtypes","unchecked"})
  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  table = newTab;
  if (oldTab != null) {
   // 下面是將舊數(shù)組中的元素,塞入新的數(shù)組中
      for (int j = 0; j < oldCap; ++j) {
          Node<K,V> e;
          if ((e = oldTab[j]) != null) {
              oldTab[j] = null;
              if (e.next == null) {
                  // 若Node節(jié)點(diǎn)沒有出現(xiàn)hash碰撞虹脯,則直接塞入新的數(shù)組
                  newTab[e.hash & (newCap - 1)] = e;
              } else if (e instanceof TreeNode) {
                  // 對于出現(xiàn)hash碰撞驴娃,且紅黑樹結(jié)構(gòu)時,需要重新分配
                  ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
              } else { // preserve order
                  Node<K,V> loHead = null, loTail = null;
                  Node<K,V> hiHead = null, hiTail = null;
                  Node<K,V> next;
                  do {
                      next = e.next;
                      if ((e.hash & oldCap) == 0) {
                      // 新的位置相比原來的新增了 oldCap
                          if (loTail == null)
                              loHead = e;
                          else
                              loTail.next = e;
                          loTail = e;
                      }
                      else { // 位置不變
                          if (hiTail == null)
                              hiHead = e;
                          else
                              hiTail.next = e;
                          hiTail = e;
                      }
                  } while ((e = next) != null);
                  if (loTail != null) {
                      loTail.next = null;
                      newTab[j] = loHead;
                  }
                  if (hiTail != null) {
                      hiTail.next = null;
                      newTab[j + oldCap] = hiHead;
                  }
              }
          }
      }
  }
  return newTab;
}

上面的邏輯主要劃分為兩塊

  • 新的數(shù)組長度確定循集,并初始化新的數(shù)組
  • 將原來的數(shù)據(jù)遷移到新的數(shù)組中
    • 遍歷舊數(shù)組元素
    • 若Node沒有尾節(jié)點(diǎn)(Next為null)唇敞,則直接塞入新的數(shù)組
    • 判斷Node的數(shù)據(jù)結(jié)構(gòu),紅黑樹和鏈表邏輯有區(qū)分
    • 對于鏈表格式,新的坐標(biāo)要么是原來的位置疆柔,要么是原來的位置+原數(shù)組長度咒精,鏈表順序不變

說明

這個擴(kuò)容的邏輯還是比較有意思的,最后面給一個測試case旷档,來看一下擴(kuò)容前后的數(shù)據(jù)位置

4. 刪除元素

刪除的邏輯和上面的大致類似模叙,顯示確定節(jié)點(diǎn),然后從整個數(shù)據(jù)結(jié)構(gòu)中移除引用

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 刪除的前置條件:
    // 1. 數(shù)組已經(jīng)初始化
    // 2. key對應(yīng)的Node節(jié)點(diǎn)存在
    if ((tab = table) != null && (n = tab.length) > 0 
      && (p = tab[index = (n - 1) & hash]) != null) { 
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash && 
          ((k = p.key) == key || (key != null && key.equals(k)))) {
          // 數(shù)組中的Node節(jié)點(diǎn)即為目標(biāo)
            node = p;
        } else if ((e = p.next) != null) {
          // hash碰撞鞋屈,目標(biāo)可能在鏈表or紅黑樹中
          // 便利鏈表or紅黑樹范咨,確定目標(biāo)
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        if (node != null && 
            (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 找到目標(biāo)節(jié)點(diǎn),直接從數(shù)組or紅黑樹or鏈表中移除
            // 不改變Node節(jié)點(diǎn)的內(nèi)容
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

測試

上面的幾個常用方法的邏輯大致相同厂庇,核心都是在如何找到目標(biāo)Node節(jié)點(diǎn)渠啊,其中比較有意思的一點(diǎn)是數(shù)組的擴(kuò)容,舊元素的遷移邏輯权旷,下面寫個測試demo來演示一下

首先定義一個Deom對象替蛉,覆蓋hashCode方法,確保第一次重新分配數(shù)組時拄氯,正好需要遷移

public static class Demo {
  public int num;

  public Demo(int num) {
      this.num = num;
  }

  @Override
  public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Demo demo = (Demo) o;

      return num == demo.num;
  }

  @Override
  public int hashCode() {
      return num % 3 + 16;
  }
}


@Test
public void testMapResize() {
  Map<Demo, Integer> map = new HashMap<>();
  for(int i = 1; i < 12; i++) {
      map.put(new Demo(i), i);
  }

  // 下面這一行執(zhí)行躲查,并不會觸發(fā)resize方法
  map.put(new Demo(12), 12);
  // 執(zhí)行下面這一行,會觸發(fā)HashMap的resize方法
  // 因為 hashCode值 & 16 == 1译柏,所以新的位置會是原來的位置+16
  map.put(new Demo(13), 13);
}

實際演示示意圖

showdemo.gif

小結(jié)

1. 根據(jù)Key定位Node節(jié)點(diǎn)

  • key計算hash镣煮,hash值對數(shù)組長度取余即為數(shù)組中的下標(biāo)
    • hash & (len - 1) === hash % len
  • 以數(shù)組中Node為鏈表頭or紅黑樹根節(jié)點(diǎn)遍歷,確認(rèn)目標(biāo)節(jié)點(diǎn)
    • 判斷邏輯:
    • hash值相同
    • key1 == key2 or key1.quals(key2)

2. 擴(kuò)容邏輯

  • 當(dāng)添加元素后艇纺,數(shù)組的長度超過閥值,實現(xiàn)擴(kuò)容
    • 初始容量為16邮弹,閥值為12
  • 計算新的數(shù)組長度黔衡,并初始化
    • 新的長度為原來的長度 * 2
    • 新的閥值為 新的長度 * loadFactor; loadFactory 一般為 0.75
  • 將原來的數(shù)據(jù)遷移到新的數(shù)組
    • 原位置不變 (hash % 原長度 == 0)
    • 原位置 + 原數(shù)組長度 (hash % 原長度 == 1)

3. 其他

  • jdk1.8 之后,當(dāng)鏈表長度超過閥值(8)后腌乡,轉(zhuǎn)為紅黑樹
  • 新增元素盟劫,在添加完畢之后,再判斷是否需要擴(kuò)容
  • 刪除元素不會改變Node對象本身与纽,只是將其從Map的數(shù)據(jù)結(jié)構(gòu)中 出來
  • Map如何退化為鏈表
    • 一個糟糕的hashCode方法即可模擬實現(xiàn)侣签,如我們上面的測試用例
    • 紅黑樹會使這種退化的效果不至于變得那么糟糕

相關(guān)博文

關(guān)注更多

掃一掃二維碼,關(guān)注 小灰灰blog

https://static.oschina.net/uploads/img/201709/22221611_Fdo5.jpg

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末急迂,一起剝皮案震驚了整個濱河市影所,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌僚碎,老刑警劉巖猴娩,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡卷中,警方通過查閱死者的電腦和手機(jī)矛双,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟆豫,“玉大人议忽,你說我怎么就攤上這事∈酰” “怎么了栈幸?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嫉称。 經(jīng)常有香客問我侦镇,道長,這世上最難降的妖魔是什么织阅? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任壳繁,我火速辦了婚禮,結(jié)果婚禮上荔棉,老公的妹妹穿的比我還像新娘闹炉。我一直安慰自己,他們只是感情好润樱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布渣触。 她就那樣靜靜地躺著,像睡著了一般壹若。 火紅的嫁衣襯著肌膚如雪嗅钻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天店展,我揣著相機(jī)與錄音养篓,去河邊找鬼。 笑死赂蕴,一個胖子當(dāng)著我的面吹牛柳弄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播概说,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼碧注,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了糖赔?” 一聲冷哼從身側(cè)響起萍丐,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎放典,沒想到半個月后碉纺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體船万,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年骨田,在試婚紗的時候發(fā)現(xiàn)自己被綠了耿导。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡态贤,死狀恐怖舱呻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悠汽,我是刑警寧澤箱吕,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站柿冲,受9級特大地震影響茬高,放射性物質(zhì)發(fā)生泄漏假抄。R本人自食惡果不足惜怎栽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宿饱。 院中可真熱鬧熏瞄,春花似錦、人聲如沸谬以。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽为黎。三九已至邮丰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铭乾,已是汗流浹背剪廉。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留片橡,地道東北人妈经。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓淮野,卻偏偏與公主長得像捧书,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子骤星,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一洞难、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,265評論 0 16
  • HashMap 是 Java 面試必考的知識點(diǎn)舆吮,面試官從這個小知識點(diǎn)就可以了解我們對 Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,667評論 9 107
  • 前言 這次我和大家一起學(xué)習(xí)HashMap,HashMap我們在工作中經(jīng)常會使用色冀,而且面試中也很頻繁會問到潭袱,因為它里...
    liangzzz閱讀 7,990評論 7 102
  • “在過往的時光里屯换,我們往往太在意一件事情麻不麻煩,劃不劃算与学,經(jīng)常忽略內(nèi)心最真實的感受彤悔,其實,真正讓人感到快樂和幸福...
    tantandou閱讀 382評論 1 1
  • 我問佛:未來的路我怎么走索守? 佛說:路在心中晕窑,不在腳下,心有多遠(yuǎn)卵佛,路有多長杨赤。 我懂了……
    呂明超閱讀 113評論 0 0