<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試:說說你對(duì) HashMap 的認(rèn)識(shí)?

          共 50044字,需瀏覽 101分鐘

           ·

          2020-10-11 14:45

          點(diǎn)擊上方藍(lán)色“程序猿DD”,選擇“設(shè)為星標(biāo)”

          回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!

          1 概述

          HashMap是基于哈希表實(shí)現(xiàn)的,每一個(gè)元素是一個(gè)key-value對(duì),其內(nèi)部通過單鏈表解決沖突問題,容量不足(超過了閥值)時(shí),同樣會(huì)自動(dòng)增長(zhǎng).
          HashMap是非線程安全的,只適用于單線程環(huán)境,多線程環(huán)境可以采用并發(fā)包下的concurrentHashMap
          HashMap 實(shí)現(xiàn)了Serializable接口,因此它支持序列化,實(shí)現(xiàn)了Cloneable接口,能被克隆
          HashMap是基于哈希表的Map接口的非同步實(shí)現(xiàn).此實(shí)現(xiàn)提供所有可選的映射操作,并允許使用null值和null鍵.此類不保證映射的順序,特別是它不保證該順序恒久不變.
          Java8中又對(duì)此類底層實(shí)現(xiàn)進(jìn)行了優(yōu)化,比如引入了紅黑樹的結(jié)構(gòu)以解決哈希碰撞

          2 HashMap的數(shù)據(jù)結(jié)構(gòu)

          在Java中,最基本的結(jié)構(gòu)就是兩種,一個(gè)是數(shù)組,另外一個(gè)是模擬指針(引用),所有的數(shù)據(jù)結(jié)構(gòu)都可以用這兩個(gè)基本結(jié)構(gòu)來構(gòu)造,HashMap也不例外. HashMap實(shí)際上是一個(gè)"鏈表散列"的數(shù)據(jù)結(jié)構(gòu),即數(shù)組和鏈表的結(jié)合體.

          HashMap的主結(jié)構(gòu)類似于一個(gè)數(shù)組,添加值時(shí)通過key確定儲(chǔ)存位置.
          每個(gè)位置是一個(gè)Entry的數(shù)據(jù)結(jié)構(gòu),該結(jié)構(gòu)可組成鏈表.
          當(dāng)發(fā)生沖突時(shí),相同hash值的鍵值對(duì)會(huì)組成鏈表.
          這種數(shù)組+鏈表的組合形式大部分情況下都能有不錯(cuò)的性能效果,Java6、7就是這樣設(shè)計(jì)的. 然而,在極端情況下,一組(比如經(jīng)過精心設(shè)計(jì)的)鍵值對(duì)都發(fā)生了沖突,這時(shí)的哈希結(jié)構(gòu)就會(huì)退化成一個(gè)鏈表,使HashMap性能急劇下降.
          所以在Java8中,HashMap的結(jié)構(gòu)實(shí)現(xiàn)變?yōu)閿?shù)組+鏈表+紅黑樹

          可以看出,HashMap底層就是一個(gè)數(shù)組結(jié)構(gòu)
          數(shù)組中的每一項(xiàng)又是一個(gè)鏈表
          當(dāng)新建一個(gè)HashMap時(shí),就會(huì)初始化一個(gè)數(shù)組.

          3 三大集合與迭代子

          HashMap使用三大集合和三種迭代子來輪詢其Key、Value和Entry對(duì)象
          public class HashMapExam {
              public static void main(String[] args{
                  Map map = new HashMap(16);
                  for (int i = 0; i < 15; i++) {
                      map.put(i, new String(new char[]{(char) ('A'+ i)}));
                  }
                  System.out.println("======keySet=======");
                  Set set = map.keySet();
                  Iterator iterator = set.iterator();
                  while (iterator.hasNext()) {
                      System.out.println(iterator.next());
                  }
                  System.out.println("======values=======");
                  Collection values = map.values();
                  Iterator stringIterator=values.iterator();
                  while (stringIterator.hasNext()) {
                      System.out.println(stringIterator.next());
                  }
                  System.out.println("======entrySet=======");
                  for (Map.Entry entry : map.entrySet()) {
                      System.out.println(entry);
                  }
              }
          }

          4 源碼分析

          //默認(rèn)的初始容量16,且實(shí)際容量是2的整數(shù)冪
              static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
              //最大容量(傳入容量過大將被這個(gè)值替換)
              static final int MAXIMUM_CAPACITY = 1 << 30;
              // 默認(rèn)加載因子為0.75(當(dāng)表達(dá)到3/4滿時(shí),才會(huì)再散列),這個(gè)因子在時(shí)間和空間代價(jià)之間達(dá)到了平衡.更高的因子可以降低表所需的空間,但是會(huì)增加查找代價(jià),而查找是最頻繁操作
              static final float DEFAULT_LOAD_FACTOR = 0.75f;
              //桶的樹化閾值:即 鏈表轉(zhuǎn)成紅黑樹的閾值,在存儲(chǔ)數(shù)據(jù)時(shí),當(dāng)鏈表長(zhǎng)度 >= 8時(shí),則將鏈表轉(zhuǎn)換成紅黑樹
              static final int TREEIFY_THRESHOLD = 8;
             // 桶的鏈表還原閾值:即 紅黑樹轉(zhuǎn)為鏈表的閾值,當(dāng)在擴(kuò)容(resize())時(shí)(HashMap的數(shù)據(jù)存儲(chǔ)位置會(huì)重新計(jì)算),在重新計(jì)算存儲(chǔ)位置后,當(dāng)原有的紅黑樹內(nèi)數(shù)量 <= 6時(shí),則將 紅黑樹轉(zhuǎn)換成鏈表
              static final int UNTREEIFY_THRESHOLD = 6;
             //最小樹形化容量閾值:即 當(dāng)哈希表中的容量 > 該值時(shí),才允許樹形化鏈表 (即 將鏈表 轉(zhuǎn)換成紅黑樹)
          因?yàn)榧t黑樹的平均查找長(zhǎng)度是log(n),長(zhǎng)度為8的時(shí)候,平均查找長(zhǎng)度為3,如果繼續(xù)使用鏈表,平均查找長(zhǎng)度為8/2=4,這才有轉(zhuǎn)換為樹的必要
          鏈表長(zhǎng)度如果是小于等于6,6/2=3,雖然速度也很快的,但是轉(zhuǎn)化為樹結(jié)構(gòu)和生成樹的時(shí)間并不會(huì)太短
          還有選擇6和8,中間有個(gè)差值7可以有效防止鏈表和樹頻繁轉(zhuǎn)換
          假設(shè)一下,如果設(shè)計(jì)成鏈表個(gè)數(shù)超過8則鏈表轉(zhuǎn)換成樹結(jié)構(gòu),鏈表個(gè)數(shù)小于8則樹結(jié)構(gòu)轉(zhuǎn)換成鏈表,如果一個(gè)HashMap不停的插入、刪除元素,鏈表個(gè)數(shù)在8左右徘徊,就會(huì)頻繁的發(fā)生樹轉(zhuǎn)鏈表、鏈表轉(zhuǎn)樹,效率會(huì)很低。
          // 為了避免擴(kuò)容/樹形化選擇的沖突,這個(gè)值不能小于 4 * TREEIFY_THRESHOLD
              // 小于該值時(shí)使用的是擴(kuò)容哦!!!
              static final int MIN_TREEIFY_CAPACITY = 64;
              // 存儲(chǔ)數(shù)據(jù)的Node數(shù)組,長(zhǎng)度是2的冪.
              // HashMap采用鏈表法解決沖突,每一個(gè)Node本質(zhì)上是一個(gè)單向鏈表
              //HashMap底層存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu),是一個(gè)Node數(shù)組.上面得知Node類為元素維護(hù)了一個(gè)單向鏈表.至此,HashMap存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)也就很清晰了:維護(hù)了一個(gè)數(shù)組,每個(gè)數(shù)組又維護(hù)了一個(gè)單向鏈表.之所以這么設(shè)計(jì),考慮到遇到哈希沖突的時(shí)候,同index的value值就用單向鏈表來維護(hù)
              //與 JDK 1.7 的對(duì)比(Entry類),僅僅只是換了名字
              transient Node[] table;
              // HashMap的底層數(shù)組中已用槽的數(shù)量
              transient int size;
              // HashMap的閾值,用于判斷是否需要調(diào)整HashMap的容量(threshold = 容量*加載因子)
              int threshold;
              // 負(fù)載因子實(shí)際大小
              final float loadFactor;
              // HashMap被改變的次數(shù)
              transient int modCount;
              // 指定“容量大小”和“加載因子”的構(gòu)造函數(shù),是最基礎(chǔ)的構(gòu)造函數(shù)
              public HashMap(int initialCapacity, float loadFactor) {
                  if (initialCapacity < 0)
                      throw new IllegalArgumentException("Illegal initial capacity: " +
                                                         initialCapacity);
                  // HashMap的最大容量只能是MAXIMUM_CAPACITY
                  if (initialCapacity > MAXIMUM_CAPACITY)
                      initialCapacity = MAXIMUM_CAPACITY;
                  //負(fù)載因子須大于0
                  if (loadFactor <= 0 || Float.isNaN(loadFactor))
                      throw new IllegalArgumentException("Illegal load factor: " +
                                                         loadFactor);
                  // 設(shè)置"負(fù)載因子"
                  this.loadFactor = loadFactor;
                  // 設(shè)置"HashMap閾值",當(dāng)HashMap中存儲(chǔ)數(shù)據(jù)的數(shù)量達(dá)到threshold時(shí),就需將HashMap的容量加倍
                  this.threshold = tableSizeFor(initialCapacity);
              }
          上面的tableSizeFor有何用?
          tableSizeFor方法保證函數(shù)返回值是大于等于給定參數(shù)initialCapacity最小的2的冪次方的數(shù)值
          static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            return (n = MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
            }
          a |= b 等同于 a = a|b
          逐行分析
          • int n = cap - 1
            給定的cap 減 1,為了避免參數(shù)cap本來就是2的冪次方,這樣一來,經(jīng)過后續(xù)操作,cap將會(huì)變成2 * cap,是不符合我們預(yù)期的
          • n |= n >>> 1
            n >>> 1 : n無符號(hào)右移1位,即n二進(jìn)制最高位的1右移一位
            n | (n >>> 1) 導(dǎo)致 n二進(jìn)制的高2位值為1
            目前n的高1~2位均為1
          • n |= n >>> 2
            n繼續(xù)無符號(hào)右移2位
            n | (n >>> 2) 導(dǎo)致n二進(jìn)制表示的高34位經(jīng)過運(yùn)算值均為1
            目前n的高14位均為1
          • n |= n >>> 4
            n繼續(xù)無符號(hào)右移4位
            n | (n >>> 4) 導(dǎo)致n二進(jìn)制表示的高58位經(jīng)過運(yùn)算值均為1
            目前n的高18位均為1
          • n |= n >>> 8
            n繼續(xù)無符號(hào)右移8位
            n | (n >>> 8) 導(dǎo)致n二進(jìn)制表示的高916位經(jīng)過運(yùn)算值均為1
            目前n的高116位均為1
          可以看出,無論給定cap(cap < MAXIMUM_CAPACITY )的值是多少,經(jīng)過以上運(yùn)算,其值的二進(jìn)制所有位都會(huì)是1.再將其加1,這時(shí)候這個(gè)值一定是2的冪次方.
          當(dāng)然如果經(jīng)過運(yùn)算值大于MAXIMUM_CAPACITY,直接選用MAXIMUM_CAPACITY.
          至此tableSizeFor如何保證cap為2的冪次方已經(jīng)顯而易見了,那么問題來了

          4.1 為什么cap要保持為2的冪次方?

          主要與HashMap中的數(shù)據(jù)存儲(chǔ)有關(guān).
          在Java8中,HashMap中key的Hash值由Hash(key)方法計(jì)得

          HashMap中存儲(chǔ)數(shù)據(jù)table的index是由key的Hash值決定的.
          在HashMap存儲(chǔ)數(shù)據(jù)時(shí),我們期望數(shù)據(jù)能均勻分布,以防止哈希沖突.
          自然而然我們就會(huì)想到去用%取余操作來實(shí)現(xiàn)我們這一構(gòu)想
          取余(%)操作 : 如果除數(shù)是2的冪次則等價(jià)于與其除數(shù)減一的與(&)操作.
          這也就解釋了為什么一定要求cap要為2的冪次方.再來看看table的index的計(jì)算規(guī)則:
          等價(jià)于:
          index = e.hash % newCap
          采用二進(jìn)制位操作&,相對(duì)于%,能夠提高運(yùn)算效率,這就是cap的值被要求為2冪次的原因

          4.2 Node類

          static class Node implements Map.Entry {
                  final int hash;
                  final K key;
                  V value;
                  Node next;
                  Node(int hash, K key, V value, Node next) {
                      this.hash = hash;
                      this.key = key;
                      this.value = value;
                      this.next = next;
                  }
                  public final K getKey()        return key; }
                  public final V getValue()      return value; }
                  public final String toString() return key + "=" + value; }
                  public final int hashCode() {
                      return Objects.hashCode(key) ^ Objects.hashCode(value);
                  }
                  public final V setValue(V newValue) {
                      V oldValue = value;
                      value = newValue;
                      return oldValue;
                  }
                  public final boolean equals(Object o) {
                      if (o == this)
                          return true;
                      if (o instanceof Map.Entry) {
                          Map.Entry e = (Map.Entry)o;
                          if (Objects.equals(key, e.getKey()) &&
                              Objects.equals(value, e.getValue()))
                              return true;
                      }
                      return false;
                  }
              }
          Node 類是HashMap中的靜態(tài)內(nèi)部類,實(shí)現(xiàn)Map.Entry接口.定義了key鍵、value值、next節(jié)點(diǎn),也就是說元素之間構(gòu)成了單向鏈表.

          4.3 TreeNode

          static final class TreeNode extends LinkedHashMap.Entry {
                  TreeNode parent; // red-black tree links
                  TreeNode left;
                  TreeNode right;
                  TreeNode prev; // needed to unlink next upon deletion
                  boolean red;
                  TreeNode(int hash, K key, V val, Node next) {}
                  // 返回當(dāng)前節(jié)點(diǎn)的根節(jié)點(diǎn)
                  final TreeNode root() {
                    for (TreeNode r = this, p;;) {
                      if ((p = r.parent) == null)
                          return r;
                      r = p;
                  }
              }
           }
          紅黑樹結(jié)構(gòu)包含前、后、左、右節(jié)點(diǎn),以及標(biāo)志是否為紅黑樹的字段
          此結(jié)構(gòu)是Java8新加的

          4.4 hash方法

          Java 8中的散列值優(yōu)化函數(shù)

          只做一次16位右位移異或
          key.hashCode()函數(shù)調(diào)用的是key鍵值類型自帶的哈希函數(shù),返回int型散列值
          理論上散列值是一個(gè)int型,如果直接拿散列值作為下標(biāo)訪問HashMap主數(shù)組的話,考慮到2進(jìn)制32位帶符號(hào)的int范圍大概40億的映射空間。只要哈希函數(shù)映射得比較均勻松散,一般應(yīng)用是很難出現(xiàn)碰撞的。
          但問題是一個(gè)40億長(zhǎng)度的數(shù)組,內(nèi)存是放不下的.HashMap擴(kuò)容之前的數(shù)組初始大小才16,所以這個(gè)散列值是不能直接拿來用的.
          用之前還要先做對(duì)數(shù)組的長(zhǎng)度取模運(yùn)算,得到的余數(shù)才能用來訪問數(shù)組下標(biāo)
          源碼中模運(yùn)算就是把散列值和數(shù)組長(zhǎng)度做一個(gè)"與"操作,

          這也正好解釋了為什么HashMap的數(shù)組長(zhǎng)度要取2的整次冪
          因?yàn)檫@樣(數(shù)組長(zhǎng)度-1)正好相當(dāng)于一個(gè)“低位掩碼”
          “與”操作的結(jié)果就是散列值的高位全部歸零,只保留低位值,用來做數(shù)組下標(biāo)訪問
          以初始長(zhǎng)度16為例,16-1=15
          2進(jìn)制表示是00000000 00000000 00001111
          和某散列值做“與”操作如下,結(jié)果就是截取了最低的四位值

          但這時(shí)候問題就來了,這樣就算我的散列值分布再松散,要是只取最后幾位的話,碰撞也會(huì)很嚴(yán)重
          這時(shí)候“擾動(dòng)函數(shù)”的價(jià)值就體現(xiàn)出來了

          右位移16位,正好是32位一半,自己的高半?yún)^(qū)和低半?yún)^(qū)做異或,就是為了混合原始hashCode的高位和低位,以此來加大低位的隨機(jī)性
          而且混合后的低位摻雜了高位的部分特征,這樣高位的信息也被變相保留下來。
          index的運(yùn)算規(guī)則是
          e.hash & (newCap - 1)
          newCap是2的冪,所以newCap - 1的高位全0
          若e.hash值只用自身的hashcode,index只會(huì)和e.hash的低位做&操作.這樣一來,index的值就只有低位參與運(yùn)算,高位毫無存在感,從而會(huì)帶來哈希沖突的風(fēng)險(xiǎn)
          所以在計(jì)算key的hashCode時(shí),用其自身hashCode與其低16位做異或操作
          這也就讓高位參與到index的計(jì)算中來了,即降低了哈希沖突的風(fēng)險(xiǎn)又不會(huì)帶來太大的性能問題

          4.5 Put方法


          ①.判斷鍵值對(duì)數(shù)組table[i]是否為空或?yàn)閚ull,否則執(zhí)行resize()進(jìn)行擴(kuò)容
          ②.根據(jù)鍵值key計(jì)算hash值得到插入的數(shù)組索引i,如果table[i]==null,直接新建節(jié)點(diǎn)添加,轉(zhuǎn)向⑥,如果table[i]不為空,轉(zhuǎn)向③
          ③.判斷table[i]的首個(gè)元素是否和key一樣,如果相同直接覆蓋value,否則轉(zhuǎn)向④,這里的相同指的是hashCode以及equals
          ④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對(duì),否則轉(zhuǎn)向⑤
          ⑤.遍歷table[i],判斷鏈表長(zhǎng)度是否大于8,大于8的話把鏈表轉(zhuǎn)換為紅黑樹,在紅黑樹中執(zhí)行插入操作,否則進(jìn)行鏈表的插入操作;遍歷過程中若發(fā)現(xiàn)key已經(jīng)存在直接覆蓋value即可
          ⑥.插入成功后,判斷實(shí)際存在的鍵值對(duì)數(shù)量size是否超多了最大容量threshold,如果超過,執(zhí)行resize()擴(kuò)容
          public V put(K key, V value{
                  // 對(duì)key的hashCode()做hash
                  return putVal(hash(key), key, valuefalsetrue);
              }
          final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict{
                  Node[] tab; Node p; int n, i;
                  // 步驟① tab為空則調(diào)用resize()初始化創(chuàng)建
                  if ((tab = table) == null || (n = tab.length) == 0)
                      n = (tab = resize()).length;
                  // 步驟② 計(jì)算index,并對(duì)null做處理
                  //tab[i = (n - 1) & hash對(duì)應(yīng)下標(biāo)的第一個(gè)節(jié)點(diǎn)
                  if ((p = tab[i = (n - 1) & hash]) == null)
                      // 無哈希沖突的情況下,將value直接封裝為Node并賦值
                      tab[i] = newNode(hash, key, valuenull);
                  else {
                      Node e; K k;
                      // 步驟③ 節(jié)點(diǎn)的key相同,直接覆蓋節(jié)點(diǎn)
                      if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                          e = p;
                      // 步驟④ 判斷該鏈為紅黑樹
                      else if (p instanceof TreeNode)
                           // p是紅黑樹類型,則調(diào)用putTreeVal方式賦值
                          e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
                      // 步驟⑤ p非紅黑樹類型,該鏈為鏈表
                      else {
                          // index 相同的情況下
                          for (int binCount = 0; ; ++binCount) {
                              if ((e = p.next) == null) {
                                  // 如果p的next為空,將新的value值添加至鏈表后面
                                  p.next = newNode(hash, key, valuenull);
                                  if (binCount >= TREEIFY_THRESHOLD - 1)
                                      // 如果鏈表長(zhǎng)度大于8,鏈表轉(zhuǎn)化為紅黑樹,執(zhí)行插入
                                      treeifyBin(tab, hash);
                                  break;
                              }
                              // key相同則跳出循環(huán)
                              if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                                  break;
                              //就是移動(dòng)指針方便繼續(xù)取 p.next
                              p = e;
                          }
                      }
                      if (e != null) { // existing mapping for key
                          V oldValue = e.value;
                          //根據(jù)規(guī)則選擇是否覆蓋value
                          if (!onlyIfAbsent || oldValue == null)
                              e.value = value;
                          afterNodeAccess(e);
                          return oldValue;
                      }
                  }
                  ++modCount;
                  // 步驟⑥:超過最大容量,就擴(kuò)容
                  if (++size > threshold)
                      // size大于加載因子,擴(kuò)容
                      resize();
                  afterNodeInsertion(evict);
                  return null;
              }
          在構(gòu)造函數(shù)中最多也只是設(shè)置了initialCapacity、loadFactor的值,并沒有初始化table,table的初始化工作是在put方法中進(jìn)行的.

          4.6 resize


          擴(kuò)容(resize)就是重新計(jì)算容量,向HashMap對(duì)象里不停的添加元素,內(nèi)部的數(shù)組無法裝載更多的元素時(shí),就需要擴(kuò)大數(shù)組的長(zhǎng)度.
          當(dāng)然Java里的數(shù)組是無法自動(dòng)擴(kuò)容的,方法是使用一個(gè)新的數(shù)組代替已有的容量小的數(shù)組
          /**
               * 該函數(shù)有2種使用情況:1.初始化哈希表 2.當(dāng)前數(shù)組容量過小,需擴(kuò)容
               */

          final Node[] resize() {
                  Node[] oldTab = table;
                  int oldCap = (oldTab == null) ? 0 : oldTab.length;
                  int oldThr = threshold;
                  int newCap, newThr = 0;
                  // 針對(duì)情況2:若擴(kuò)容前的數(shù)組容量超過最大值,則不再擴(kuò)充
                  if (oldCap > 0) {
                      if (oldCap >= MAXIMUM_CAPACITY) {
                          threshold = Integer.MAX_VALUE;
                          return oldTab;
                      }
                      // 針對(duì)情況2:若無超過最大值,就擴(kuò)充為原來的2倍
                      else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                               oldCap >= DEFAULT_INITIAL_CAPACITY)
                          //newCap設(shè)置為oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默認(rèn)值, 新的threshold增加為原來的2倍
                          newThr = oldThr << 1// double threshold
                  }
                  // 針對(duì)情況1:初始化哈希表(采用指定 or 默認(rèn)值)
                  else if (oldThr > 0// initial capacity was placed in threshold
                      // threshold>0, 將threshold設(shè)置為newCap,所以要用tableSizeFor方法保證threshold是2的冪次方
                      newCap = oldThr;
                  else { // zero initial threshold signifies using defaults
                      // 默認(rèn)初始化
                      newCap = DEFAULT_INITIAL_CAPACITY;
                      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
                  }
                  // 計(jì)算新的resize上限
                  if (newThr == 0) {
                      // newThr為0,newThr = newCap * 0.75
                      float ft = (float)newCap * loadFactor;
                      newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                                (int)ft : Integer.MAX_VALUE);
                  }
                  threshold = newThr;
                  @SuppressWarnings({"rawtypes","unchecked"})
                      // 新生成一個(gè)table數(shù)組
                      Node[] newTab = (Node[])new Node[newCap];
                  table = newTab;
                  if (oldTab != null) {
                      // oldTab 復(fù)制到 newTab
                      for (int j = 0; j < oldCap; ++j) {
                          Node e;
                          if ((e = oldTab[j]) != null) {
                              oldTab[j] = null;
                              if (e.next == null)
                                 // 鏈表只有一個(gè)節(jié)點(diǎn),直接賦值
                                 //為什么要重新Hash呢?因?yàn)殚L(zhǎng)度擴(kuò)大以后,Hash的規(guī)則也隨之改變。
                                  newTab[e.hash & (newCap - 1)] = e;
                              else if (e instanceof TreeNode)
                                  // e為紅黑樹的情況
                                  ((TreeNode)e).split(this, newTab, j, oldCap);
                              else { // preserve order鏈表優(yōu)化重hash的代碼塊
                                  Node loHead = null, loTail = null;
                                  Node hiHead = null, hiTail = null;
                                  Node next;
                                  do {
                                      next = e.next;
                                      // 原索引
                                      if ((e.hash & oldCap) == 0) {
                                          if (loTail == null)
                                              loHead = e;
                                          else
                                              loTail.next = e;
                                          loTail = e;
                                      }
                                      // 原索引 + oldCap
                                      else {
                                          if (hiTail == null)
                                              hiHead = e;
                                          else
                                              hiTail.next = e;
                                          hiTail = e;
                                      }
                                  } while ((e = next) != null);
                                  // 原索引放到bucket里
                                  if (loTail != null) {
                                      loTail.next = null;
                                      newTab[j] = loHead;
                                  }
                                  // 原索引+oldCap放到bucket里
                                  if (hiTail != null) {
                                      hiTail.next = null;
                                      newTab[j + oldCap] = hiHead;
                                  }
                              }
                          }
                      }
                  }
                  return newTab;
              }

          4.7 remove方法

          remove(key) 方法 和 remove(key, value) 方法都是通過調(diào)用removeNode的方法來實(shí)現(xiàn)刪除元素的
          final Node removeNode(int hash, Object key, Object value,
                                         boolean matchValue, boolean movable
          {
                  Node[] tab; Node p; int n, index;
                  if ((tab = table) != null && (n = tab.length) > 0 &&
                      (p = tab[index = (n - 1) & hash]) != null) {
                      Node node = null, e; K k; V v;
                      if (p.hash == hash &&
                          ((k = p.key) == key || (key != null && key.equals(k))))
                          // index 元素只有一個(gè)元素
                          node = p;
                      else if ((e = p.next) != null) {
                          if (p instanceof TreeNode)
                              // index處是一個(gè)紅黑樹
                              node = ((TreeNode)p).getTreeNode(hash, key);
                          else {
                              // index處是一個(gè)鏈表,遍歷鏈表返回node
                              do {
                                  if (e.hash == hash &&
                                      ((k = e.key) == key ||
                                       (key != null && key.equals(k)))) {
                                      node = e;
                                      break;
                                  }
                                  p = e;
                              } while ((e = e.next) != null);
                          }
                      }
                      // 分不同情形刪除節(jié)點(diǎn)
                      if (node != null && (!matchValue || (v = node.value) == value ||
                                           (value != null && value.equals(v)))) {
                          if (node instanceof TreeNode)
                              ((TreeNode)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;
              }

          4.8 get

          /**
             * 函數(shù)原型
             * 作用:根據(jù)鍵key,向HashMap獲取對(duì)應(yīng)的值
             */
           
             map.get(key);
           /**
             * 源碼分析
             */
           
             public V get(Object key{
              Node e;
              // 1\. 計(jì)算需獲取數(shù)據(jù)的hash值
              // 2\. 通過getNode()獲取所查詢的數(shù)據(jù) ->>分析1
              // 3\. 獲取后,判斷數(shù)據(jù)是否為空
              return (e = getNode(hash(key), key)) == null ? null : e.value;
          }
          /**
             * 分析1:getNode(hash(key), key))
             */
           
          final Node getNode(int hash, Object key{
              Node[] tab; Node first, e; int n; K k;
              // 1\. 計(jì)算存放在數(shù)組table中的位置
              if ((tab = table) != null && (n = tab.length) > 0 &&
                  (first = tab[(n - 1) & hash]) != null) {
                  // 4\. 通過該函數(shù),依次在數(shù)組、紅黑樹、鏈表中查找(通過equals()判斷)
                  // a. 先在數(shù)組中找,若存在,則直接返回
                  if (first.hash == hash && // always check first node
                      ((k = first.key) == key || (key != null && key.equals(k))))
                      return first;
                  // b. 若數(shù)組中沒有,則到紅黑樹中尋找
                  if ((e = first.next) != null) {
                      // 在樹中g(shù)et
                      if (first instanceof TreeNode)
                          return ((TreeNode)first).getTreeNode(hash, key);
                      // c. 若紅黑樹中也沒有,則通過遍歷,到鏈表中尋找
                      do {
                          if (e.hash == hash &&
                              ((k = e.key) == key || (key != null && key.equals(k))))
                              return e;
                      } while ((e = e.next) != null);
                  }
              }
              return null;
          }
          在JDK1.7及以前的版本中,HashMap里是沒有紅黑樹的實(shí)現(xiàn)的,在JDK1.8中加入了紅黑樹是為了防止哈希表碰撞攻擊,當(dāng)鏈表鏈長(zhǎng)度為8時(shí),及時(shí)轉(zhuǎn)成紅黑樹,提高map的效率
          如果某個(gè)桶中的記錄過大的話(當(dāng)前是TREEIFY_THRESHOLD = 8),HashMap會(huì)動(dòng)態(tài)的使用一個(gè)專門的treemap實(shí)現(xiàn)來替換掉它。這樣做的結(jié)果會(huì)更好,是O(logn),而不是糟糕的O(n)。它是如何工作的?前面產(chǎn)生沖突的那些KEY對(duì)應(yīng)的記錄只是簡(jiǎn)單的追加到一個(gè)鏈表后面,這些記錄只能通過遍歷來進(jìn)行查找。但是超過這個(gè)閾值后HashMap開始將列表升級(jí)成一個(gè)二叉樹,使用哈希值作為樹的分支變量,如果兩個(gè)哈希值不等,但指向同一個(gè)桶的話,較大的那個(gè)會(huì)插入到右子樹里。如果哈希值相等,HashMap希望key值最好是實(shí)現(xiàn)了Comparable接口的,這樣它可以按照順序來進(jìn)行插入。這對(duì)HashMap的key來說并不是必須的,不過如果實(shí)現(xiàn)了當(dāng)然最好。如果沒有實(shí)現(xiàn)這個(gè)接口,在出現(xiàn)嚴(yán)重的哈希碰撞的時(shí)候,你就并別指望能獲得性能提升了。
          這個(gè)性能提升有什么用處?比方說惡意的程序,如果它知道我們用的是哈希算法,它可能會(huì)發(fā)送大量的請(qǐng)求,導(dǎo)致產(chǎn)生嚴(yán)重的哈希碰撞。然后不停的訪問這些key就能顯著的影響服務(wù)器的性能,這樣就形成了一次拒絕服務(wù)攻擊(DoS)。JDK 8中從O(n)到O(logn)的飛躍,可以有效地防止類似的攻擊,同時(shí)也讓HashMap性能的可預(yù)測(cè)性稍微增強(qiáng)了一些
          /**
             * 源碼分析:resize(2 * table.length)
             * 作用:當(dāng)容量不足時(shí)(容量 > 閾值),則擴(kuò)容(擴(kuò)到2倍)
             */
           
             void resize(int newCapacity{
              // 1\. 保存舊數(shù)組(old table)
              Entry[] oldTable = table;
              // 2\. 保存舊容量(old capacity ),即數(shù)組長(zhǎng)度
              int oldCapacity = oldTable.length;
              // 3\. 若舊容量已經(jīng)是系統(tǒng)默認(rèn)最大容量了,那么將閾值設(shè)置成整型的最大值,退出
              if (oldCapacity == MAXIMUM_CAPACITY) {
                  threshold = Integer.MAX_VALUE;
                  return;
              }
              // 4\. 根據(jù)新容量(2倍容量)新建1個(gè)數(shù)組,即新table
              Entry[] newTable = new Entry[newCapacity];
              // 5\. (重點(diǎn)分析)將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新table中,從而完成擴(kuò)容 ->>分析1.1
              transfer(newTable);
              // 6\. 新數(shù)組table引用到HashMap的table屬性上
              table = newTable;
              // 7\. 重新設(shè)置閾值
              threshold = (int)(newCapacity * loadFactor);
          }
           /**
             * 分析1.1:transfer(newTable);
             * 作用:將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新table中,從而完成擴(kuò)容
             * 過程:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
             */
           
          void transfer(Entry[] newTable{
                // 1\. src引用了舊數(shù)組
                Entry[] src = table;
                // 2\. 獲取新數(shù)組的大小 = 獲取新容量大小
                int newCapacity = newTable.length;
                // 3\. 通過遍歷 舊數(shù)組,將舊數(shù)組上的數(shù)據(jù)(鍵值對(duì))轉(zhuǎn)移到新數(shù)組中
                for (int j = 0; j < src.length; j++) {
                    // 3.1 取得舊數(shù)組的每個(gè)元素
                    Entry e = src[j];
                    if (e != null) {
                        // 3.2 釋放舊數(shù)組的對(duì)象引用(for循環(huán)后,舊數(shù)組不再引用任何對(duì)象)
                        src[j] = null;
                        do {
                            // 3.3 遍歷 以該數(shù)組元素為首 的鏈表
                            // 注:轉(zhuǎn)移鏈表時(shí),因是單鏈表,故要保存下1個(gè)結(jié)點(diǎn),否則轉(zhuǎn)移后鏈表會(huì)斷開
                            Entry next = e.next;
                           // 3.3 重新計(jì)算每個(gè)元素的存儲(chǔ)位置
                           int i = indexFor(e.hash, newCapacity);
                           // 3.4 將元素放在數(shù)組上:采用單鏈表的頭插入方式 = 在鏈表頭上存放數(shù)據(jù) = 將數(shù)組位置的原有數(shù)據(jù)放在后1個(gè)指針、將需放入的數(shù)據(jù)放到數(shù)組位置中
                           // 即 擴(kuò)容后,可能出現(xiàn)逆序:按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入
                           e.next = newTable[i];
                           newTable[i] = e;
                           // 訪問下1個(gè)Entry鏈上的元素,如此不斷循環(huán),直到遍歷完該鏈表上的所有節(jié)點(diǎn)
                           e = next;
                       } while (e != null);
                       // 如此不斷循環(huán),直到遍歷完數(shù)組上的所有數(shù)據(jù)元素
                   }
               }
           }
          從上面可看出:在擴(kuò)容resize()過程中,在將舊數(shù)組上的數(shù)據(jù) 轉(zhuǎn)移到 新數(shù)組上時(shí),轉(zhuǎn)移數(shù)據(jù)操作 = 按舊鏈表的正序遍歷鏈表、在新鏈表的頭部依次插入,即在轉(zhuǎn)移數(shù)據(jù)、擴(kuò)容后,容易出現(xiàn)鏈表逆序的情況
          設(shè)重新計(jì)算存儲(chǔ)位置后不變,即擴(kuò)容前 = 1->2->3,擴(kuò)容后 = 3->2->1
          此時(shí)若并發(fā)執(zhí)行 put 操作,一旦出現(xiàn)擴(kuò)容情況,則 容易出現(xiàn) 環(huán)形鏈表,從而在獲取數(shù)據(jù)、遍歷鏈表時(shí) 形成死循環(huán)(Infinite Loop),即死鎖

          單線程rehash

          單線程情況下,rehash無問題

          多線程并發(fā)下的rehash

          這里假設(shè)有兩個(gè)線程同時(shí)執(zhí)行了put操作并引發(fā)了rehash,執(zhí)行了transfer方法,并假設(shè)線程一進(jìn)入transfer方法并執(zhí)行完next = e.next后,因?yàn)榫€程調(diào)度所分配時(shí)間片用完而“暫?!?,此時(shí)線程二完成了transfer方法的執(zhí)行。此時(shí)狀態(tài)如下。

          接著線程1被喚醒,繼續(xù)執(zhí)行第一輪循環(huán)的剩余部分
          e.next = newTable[1] = null
          newTable[1] = e = key(5)
          e = next = key(9)
          結(jié)果如下圖所示

          接著執(zhí)行下一輪循環(huán),結(jié)果狀態(tài)圖如下所示

          繼續(xù)下一輪循環(huán),結(jié)果狀態(tài)圖如下所示

          此時(shí)循環(huán)鏈表形成,并且key(11)無法加入到線程1的新數(shù)組。在下一次訪問該鏈表時(shí)會(huì)出現(xiàn)死循環(huán)。

          Fast-fail

          產(chǎn)生原因

          在使用迭代器的過程中如果HashMap被修改,那么ConcurrentModificationException將被拋出,也即Fast-fail策略。
          當(dāng)HashMap的iterator()方法被調(diào)用時(shí),會(huì)構(gòu)造并返回一個(gè)新的EntryIterator對(duì)象,并將EntryIterator的expectedModCount設(shè)置為HashMap的modCount(該變量記錄了HashMap被修改的次數(shù))。
          HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
            Entry[] t = table;
            while (index < t.length && (next = t[index++]) == null)
              ;
            }
          }
          在通過該Iterator的next方法訪問下一個(gè)Entry時(shí),它會(huì)先檢查自己的expectedModCount與HashMap的modCount是否相等,如果不相等,說明HashMap被修改,直接拋出ConcurrentModificationException。該Iterator的remove方法也會(huì)做類似的檢查。該異常的拋出意在提醒用戶及早意識(shí)到線程安全問題。

          線程安全解決方案

          單線程條件下,為避免出現(xiàn)ConcurrentModificationException,需要保證只通過HashMap本身或者只通過Iterator去修改數(shù)據(jù),不能在Iterator使用結(jié)束之前使用HashMap本身的方法修改數(shù)據(jù)。因?yàn)橥ㄟ^Iterator刪除數(shù)據(jù)時(shí),HashMap的modCount和Iterator的expectedModCount都會(huì)自增,不影響二者的相等性。如果是增加數(shù)據(jù),只能通過HashMap本身的方法完成,此時(shí)如果要繼續(xù)遍歷數(shù)據(jù),需要重新調(diào)用iterator()方法從而重新構(gòu)造出一個(gè)新的Iterator,使得新Iterator的expectedModCount與更新后的HashMap的modCount相等。
          多線程條件下,可使用Collections.synchronizedMap方法構(gòu)造出一個(gè)同步Map,或者直接使用線程安全的ConcurrentHashMap。



          往期推薦

          我去!微信竟然可以查出行軌跡了,預(yù)計(jì)又一波情侶要分手?

          解決Maven依賴沖突的好幫手,這款I(lǐng)DEA插件了解一下?

          Java程序員必備的11大IntelliJ插件

          Spring Boot 注解大全,一鍵收藏!回城路上復(fù)習(xí)!

          還剩10天,趕緊登下百度網(wǎng)盤,拯救你的2T存儲(chǔ)空間吧!


          更多后端基礎(chǔ)知識(shí)與面試題

          掃描下方二維碼關(guān)注

          一起進(jìn)步大廠面基!

          瀏覽 47
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩深喉视频 | 在线视频中文字幕一区 | 黄色视频观看免费 | 性感美女操逼视频 | 殴美成人性爱大片免费看 |