java Map及其實現類的底層原理


筆記來源: 尚硅谷

一、Map接口及其多個實現類的對比

首先看下Map及其實現類的類圖關系:

圖片

Map:雙列數據,存儲key-value對的數據

實現類 含義
HashMap 一般來說作為Map的主要實現類, 線程不安全效率高可以存儲null的key和value
① JDK7及以前:數組+鏈表
② JDK8:數組+鏈表+紅黑樹
LinkedHashMap 保證在遍歷map元素時,可以按照添加的順序實現遍歷。在原有的HashMap基礎上,添加了一對指針,指向前一個和后一個元素。對於頻繁的遍歷操作,此類執行效率高於HashMap
Hashtable 古老的實現類【甚至命名都不規范】,現在已經不怎么用了, 線程安全,效率低,不能存儲null的key和value
TreeMap 存儲有序的鍵值對,實現排序遍歷【按照key排】,底層使用紅黑樹
Properties Hashtable子類,常用來處理配置文件,key和value都是string類型

常見面試題:

  • HashMap的底層實現原理

  • HashMap 和 Hashtable異同

  • ConcurrentHashMap 與 Hashtable區別

二、Map中存儲的key-value特點

圖片

  1. Map中的key是無序的,不可重復的 --> key所在的類要重寫equals()和hashCode()方法【以HashMap為例】

  2. Map中的value是無序的,可重復的 --> value所在類要重寫equals()fangfa1

一個鍵值對:key-value構成了一個Entry對象,是無序不可重復的,用set存放

三、HashMap在JDK7中的底層原理

HashMap map = new HashMap(); 

在實例化以后,底層創建了長度是16的一維數組 Entry[] table.

map.put(key1,value1) 

調用key1 所在類的hashCode()計算key1哈希值,此哈希值經過某種算法計算以后,得到在Entry數組中的存放位置。

① 如果此位置數據為空,則key1-value1添加成功 【情況1】
② 如果此位置數據不為空,(意味者此位置上存在一個或多個數據(以鏈表形式存在)),比較key1與已經存在的一個或多個數據的哈希值,

  • 如果key1的哈希值與已經存在的哈希值都不相同,此時添加成功 【情況2】
  • 如果key1的哈希值與已經存在的哈希值都相同,繼續比較,調用key1所在類的equals方 法,如果equals返回false,則key1-value1添加成功;如果equals返回true,則用value1替換相同key的value值 【情況3】

補充:關於 情況2情況3 :此時key1-value1 和原來的數據以 鏈表 的方式存儲。【見下圖】
在不斷的添加過程中,會涉及到擴容問題,默認的擴容方式:擴容為原來容量的2倍,並將原有的數據復制過來。

圖片

四、HashMap在JDK8中的底層原理

jdk8相較於jdk7在底層實現方面的不同:

1. new HashMap():底層沒有創建一個長度為16的數組 
2. jdk8 底層的數組是:Node[],而非Entry[] 
3. 首次調用put()方法時,底層創建長度為16的數組 
4. 原來jdk7底層結構只有數組+鏈表,jdk8是數組+鏈表+紅黑樹 
   當數組的某一個索引位置上的元素以鏈表形式存在的個數 > 8,且 
   當前數組的長度 > 64時,此時此索引位置上的所有數據改為使用紅黑樹 
   存儲【方便查找】。 

五、HashMap在JDK7中的底層源碼

面試題:
談談你對 HashMap 中 put/get 方法的認識?如果了解再談談HashMap的擴容機制?默認大小是多少?什么是負載因子(或填充比)?什么是吞吐臨界值(或閾值、threshold)?

HashMap源碼中的重要常量 
DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16 
MAXIMUM_ CAPACITY : HashMap的最大支持容量,2^30 
DEFAULT_LOAD_FACTOR: HashMap的默認加載因子 0.75 
TREEIFY_THRESHOLD: Bucket中鏈表長度大於該默認值 8,轉化為紅黑樹 
UNTREEIFY_THRESHOLD: Bucket中紅 黑樹存儲的Node小於該默認值,轉化為鏈表 
MIN_TREEIFY_CAPACITY:桶中的Node被樹化時最小的hash表容量。(當桶中Node的 
數量大到需要變紅黑樹時,若hash表容量小於MIN_TREEIFY_CAPACITY時,此時應執行 
resize擴容操作這個MIN_TREEIFY_CAPACITY的值至少是TREEIFY_THRESHOLD的4 
倍。) 

table: 存儲元素的數組,總是2的n次冪 
entrySet: 存儲具體元素的集 
size: HashMap中 存儲的鍵值對的數量 
modCount: HashMap擴 容和結構改變的次數。 
threshold: 擴容的臨界值,=容量*填充因子 
loadFactor: 填充因子 

5.1 構造器

//DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16 
//DEFAULT_LOAD_FACTOR: HashMap的默認加載因子,0.75 
public HashMap() ( 
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 
} 
public HashMap(int initialCapacity, float loadFactor) { 
      if (initialCapacity < 0) 
          throw new IllegalArgumentException("Illegal initial capacity: " + 
                                             initialCapacity); 
      if (initialCapacity > MAXIMUM_CAPACITY) 
          initialCapacity = MAXIMUM_CAPACITY; 
      if (loadFactor <= 0 || Float.isNaN(loadFactor)) 
          throw new IllegalArgumentException("Illegal load factor: " + 
                                             loadFactor); 
      // 找到一個大於等於initialCapacity並且是2的指數的值作為初始容量 
      // 所以你如果傳進來15,那么容量為16 
      int capacity = 1; 
      while (capacity < initialCapacity) 
          capacity <<= 1; 

      this.loadFactor = loadFactor;//加載因子,默認0.75 
      // 初始化閾值【當占用到閾值的時候擴容】 
      threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 
      // 初始化Entry數組 
      table = new Entry[capacity]; 
      useAltHashing = sun.misc.VM.isBooted() && 
              (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 
      init(); 
  } 

5.2 put方法

解析都在代碼里 ↓

public V put(K key, V value) { 
  //如果key為null,其實也往里面放了 
  if (key == null)  
      return putForNullKey(value); 
  //計算當前key的哈希值 
  int hash = hash(key); 
  // 查找對應的數組下標【hash & (length-1)】 
  int i = indexFor(hash, table.length); 
   
  for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
      //如果在i位置上已經有元素了,對比這個位置上的所有元素 
      Object k; 
      if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
          //覆蓋原有value 
          V oldValue = e.value; 
          e.value = value; 
          e.recordAccess(this); 
          return oldValue; 
      } 
  } 
  // 沒有在相同hash值的鏈表中找到key相同的節點 
  modCount++; 
  addEntry(hash, key, value, i); // 在i位置對應的鏈表上添加一個節點 
  return null; 
} 

void addEntry(int hash, K key, V value, int bucketIndex) { 
    //如果數據大小已經超過閾值[16*0.75]並且數組對應的bucket不為空,則需要擴容 
    if ((size >= threshold) && (null != table[bucketIndex])) { 
        // 擴容2倍 
        resize(2 * table.length);  
        // key為null的時,hash值設為0 
        hash = (null != key) ? hash(key) : 0;  
        // 確定是哪一個鏈表(bucket下標) 
        bucketIndex = indexFor(hash, table.length);  
    } 
    createEntry(hash, key, value, bucketIndex); 
} 
//這里需要注意!!! 
//使用的是頭插法 
void createEntry(int hash, K key, V value, int bucketIndex) { 
      Entry<K,V> e = table[bucketIndex]; 
      table[bucketIndex] = new Entry<>(hash, key, value, e); 
      size++; 
  } 
private static class Entry<K,V> implements Map.Entry<K,V> { 
    final int hash; 
    final K key; 
    V value; 
    Entry<K,V> next; 
    protected Entry(int hash, K key, V value, Entry<K,V> next) { 
        this.hash = hash; 
        this.key =  key; 
        this.value = value; 
        this.next = next; 
    } 
    //略 
    } 

六、HashMap在JDK8的源碼分析

重要常量:

DEFAULT_INITIAL_CAPACITY: HashMap的默認容量,16 
MAXIMUM_ CAPACITY : HashMap的最大支持容量,2^30 
DEFAULT_LOAD_FACTOR: HashMap的默認加載因子0.75 
TREEIFY_THRESHOLD: Bucket中鏈表長度大於該默認值,轉化為紅黑樹 

面試題:為什么不在HashMap快滿的時候再擴?

盡量讓數組出現鏈表的情況少一些

6.1 構造器

/** 
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity 
 * (16) and the default load factor (0.75). 
 */ 
public HashMap() { 
    // all other fields defaulted 
    //底層並沒有創建長度為16的數組 
    this.loadFactor = DEFAULT_LOAD_FACTOR;  
} 

在JDK8中已經不是Entry數組了,而是Node數組

transient Node<K,V>[] table; 

6.2 put

public V put(K key, V value) { 
    return putVal(hash(key), key, value, false, true); 
} 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 
               boolean evict) { 
    Node<K,V>[] tab; Node<K,V> p; int n, i; 
    if ((tab = table) == null || (n = tab.length) == 0) 
        //首次調用或者長度為0,則擴容 
        n = (tab = resize()).length; 
         
    if ((p = tab[i = (n - 1) & hash]) == null) 
        //找到在數組中存的位置,如果這個位置沒有元素   
        tab[i] = newNode(hash, key, value, null); 
     
     
    else { 
        Node<K,V> e; K k; 
        if (p.hash == hash && 
            ((k = p.key) == key || (key != null && key.equals(k)))) 
            //hash值相等 
            e = p; 
        else if (p instanceof TreeNode) 
            //是否紅黑樹 
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
        else { 
            //循環判斷有沒有hash相等的 
            for (int binCount = 0; ; ++binCount) { 
                if ((e = p.next) == null) { 
                    //尾插法 
                    p.next = newNode(hash, key, value, null); 
                    //當鏈表長度超過8時,變成紅黑樹 
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 
                        treeifyBin(tab, hash); 
                    break; 
                } 
                if (e.hash == hash && 
                    ((k = e.key) == key || (key != null && key.equals(k)))) 
                    break; 
                p = e; 
            } 
        } 
        if (e != null) { // existing mapping for key 
        //替換 
            V oldValue = e.value; 
            if (!onlyIfAbsent || oldValue == null) 
                e.value = value; 
            afterNodeAccess(e); 
            return oldValue; 
        } 
    } 
    ++modCount; 
    if (++size > threshold) 
        resize(); 
    afterNodeInsertion(evict); 
    return null; 
} 
//當數組某個位置鏈表超過8時,且當前數組長度超過64時,做成紅黑樹 
final void treeifyBin(Node<K,V>[] tab, int hash) { 
    int n, index; Node<K,V> e; 
    //MIN_TREEIFY_CAPACITY=64 
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
        resize(); 
    else if ((e = tab[index = (n - 1) & hash]) != null) { 
        TreeNode<K,V> hd = null, tl = null; 
        do { 
            TreeNode<K,V> p = replacementTreeNode(e, null); 
            if (tl == null) 
                hd = p; 
            else { 
                p.prev = tl; 
                tl.next = p; 
            } 
            tl = p; 
        } while ((e = e.next) != null); 
        if ((tab[index] = hd) != null) 
            hd.treeify(tab); 
    } 
} 

七、LinkedHashMap的底層實現

LinkedHashMap 沒有重寫 put 和 putVal,而是重寫了 putVal 中調用的 newNode 方法:

//專門用了一個數據結構 LinkedHashMap.Entry<K,V> 存儲節點,並且存儲了前一個和下一個元素 
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> p = 
        new LinkedHashMap.Entry<K,V>(hash, key, value, e); 
    linkNodeLast(p); 
    return p; 
} 
static class Entry<K,V> extends HashMap.Node<K,V> { 
    //記錄前一個節點和后一個節點 
    Entry<K,V> before, after; 
    Entry(int hash, K key, V value, Node<K,V> next) { 
        super(hash, key, value, next); 
    } 
} 


免責聲明!

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



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