JDK1.8源碼(三)——java.util.HashMap


什么是哈希表?

在討論哈希表之前,我們先大概了解下其他數據結構在新增,查找等基礎操作執行性能

  數組:采用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間復雜度為O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間復雜度為O(n),當然,對於有序數組,則可采用二分查找,插值查找,斐波那契查找等方式,可將查找復雜度提高為O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均復雜度也為O(n)

  線性鏈表:對於鏈表的新增,刪除等操作(在找到指定操作位置后),僅需處理結點間的引用即可,時間復雜度為O(1),而查找操作需要遍歷鏈表逐一進行比對,復雜度為O(n)

  二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均復雜度均為O(logn)。

  哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希沖突的情況下,僅需一次定位即可完成,時間復雜度為O(1),接下來我們就來看看哈希表是如何實現達到驚艷的常數階O(1)的。

  我們知道,數據結構的物理存儲結構只有兩種:順序存儲結構鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面我們提到過,在數組中根據下標查找某個元素,一次定位就可以達到,哈希表利用了這種特性,哈希表的主干就是數組

  比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字 通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作。

        存儲位置 = f(關鍵字)

  其中,這個函數f一般稱為哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,比如我們要在哈希表中執行插入操作:

  查找操作同理,先通過哈希函數計算出實際存儲地址,然后從數組中對應地址取出即可。

哈希沖突

  然而萬事無完美,如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎么辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞。前面我們提到過,哈希函數的設計至關重要,好的哈希函數會盡可能地保證 計算簡單散列地址分布均勻,但是,我們需要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生沖突。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發生沖突,繼續尋找下一塊未被占用的存儲地址),再散列函數法,鏈地址法,而HashMap即是采用了鏈地址法,也就是數組+鏈表的方式。

 

什么是HashMap?

HashMap 是一個利用哈希表原理來存儲元素的集合。遇到沖突時,HashMap 是采用的鏈地址法來解決,在 JDK1.7 中,HashMap 是由 數組+鏈表構成的。但是在 JDK1.8 中,HashMap 是由 數組+鏈表+紅黑樹構成,新增了紅黑樹作為底層數據結構,結構變得復雜了,但是效率也變的更高效。下面我們來具體介紹在 JDK1.8 中 HashMap 是如何實現的。

HashMap定義

HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射,而且 key 和 value 都可以為 null。

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

 

 

 藍色實線箭頭是指Class繼承關系

 綠色實線箭頭是指interface繼承關系

 綠色虛線箭頭是指接口實現關系

 

字段屬性

//默認 HashMap 集合初始容量為16(必須是 2 的倍數)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//集合的最大容量,如果通過帶參構造指定的最大容量超過此數,默認還是使用此數
static final int MAXIMUM_CAPACITY = 1 << 30;
//默認的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當桶(bucket)上的結點數大於這個值時會轉成紅黑樹(JDK1.8新增)
static final int TREEIFY_THRESHOLD = 8;
//當桶(bucket)上的節點數小於這個值時會轉成鏈表(JDK1.8新增)
static final int UNTREEIFY_THRESHOLD = 6;
/**(JDK1.8新增)
 * 當集合中的容量大於這個值時,表中的桶才能進行樹形化 ,否則桶內元素太多時會擴容,
 * 而不是樹形化 為了避免進行擴容、樹形化選擇的沖突,這個值不能小於 4 * TREEIFY_THRESHOLD
 */
static final int MIN_TREEIFY_CAPACITY = 64;
View Code
 //初始化使用,長度總是 2的冪
transient Node<K,V>[] table;
 //保存緩存的entrySet()
transient Set<Map.Entry<K,V>> entrySet;
 //此映射中包含的鍵值映射的數量。(集合存儲鍵值對的數量)
transient int size;
/**
 * 跟前面ArrayList和LinkedList集合中的字段modCount一樣,記錄集合被修改的次數
 * 主要用於迭代器中的快速失敗
 */
transient int modCount;
 //調整大小的下一個大小值(容量*加載因子)。capacity * load factor
int threshold;
 //散列表的加載因子。
final float loadFactor;
View Code

 

  ①、Node<K,V>[] table

  我們說 HashMap 是由數組+鏈表+紅黑樹組成,這里的數組就是 table 字段。后面對其進行初始化長度默認是 DEFAULT_INITIAL_CAPACITY= 16。

  ②、size

  集合中存放key-value 的實時對數。

  ③、loadFactor

  裝載因子,是用來衡量 HashMap 滿的程度,計算HashMap的實時裝載因子的方法為:size/capacity,而不是占用桶的數量去除以capacity。capacity 是桶的數量,也就是 table 的長度length。

  默認的負載因子0.75 是對空間和時間效率的一個平衡選擇,建議大家不要修改,除非在時間和空間比較特殊的情況下,如果內存空間很多而又對時間效率要求很高,可以降低負載因子loadFactor 的值;相反,如果內存空間緊張而對時間效率要求不高,可以增加負載因子 loadFactor 的值,這個值可以大於1。

  ④、threshold

  計算公式:capacity * loadFactor。這個值是當前已占用數組長度的最大值。過這個數目就重新resize(擴容),擴容后的 HashMap 容量是之前容量的兩倍

 

構造函數

①、無參構造函數

/**
 * 默認構造函數,初始化加載因子loadFactor = 0.75
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}
View Code

②、指定初始容量的構造函數

/**
 * 
 * @param initialCapacity 指定初始化容量
 * @param loadFactor 加載因子 0.75
 */
public HashMap(int initialCapacity, float loadFactor) {
    //初始化容量不能小於 0 ,否則拋出異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果初始化容量大於2的30次方,則初始化容量都為2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //如果加載因子小於0,或者加載因子是一個非數值,拋出異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //tableSizeFor()的主要功能是返回一個比給定整數大且最接近的2的冪次方整數,如給定10,返回2的4次方16.
    this.threshold = tableSizeFor(initialCapacity);
}
// 返回大於等於initialCapacity的最小的二次冪數值。
// >>> 操作符表示無符號右移,高位取0。
// | 按位或運算
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 < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
View Code

添加元素

//hash(key)獲取Key的哈希值,equls返回為true,則兩者的hashcode一定相等,意即相等的對象必須具有相等的哈希碼。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
 * 
 * @param hash Key的哈希值
 * @param key  鍵
 * @param value  值
 * @param onlyIfAbsent true 表示不要更改現有值
 * @param evict false表示table處於創建模式
 * @return
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
        boolean evict) {
     Node<K,V>[] tab; Node<K,V> p; int n, i;
     //如果table為null或者長度為0,則進行初始化
     //resize()方法本來是用於擴容,由於初始化沒有實際分配空間,這里用該方法進行空間分配,后面會詳細講解該方法
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     //(n - 1) & hash:確保索引在數組范圍內,相當於hash % n 的值
     //通過 key 的 hash code 計算其在數組中的索引:為什么不直接用 hash 對 數組長度取模?因為除法運算效率低
     if ((p = tab[i = (n - 1) & hash]) == null)
         tab[i] = newNode(hash, key, value, null);//tab[i] 為null,直接將新的key-value插入到計算的索引i位置
     else {//tab[i] 不為null,表示該位置已經有值了
         Node<K,V> e; K k;
         //e節點表示已經存在Key的節點,需要覆蓋value的節點
         //table[i]的首個元素是否和key一樣,如果相同直接覆蓋value
         if (p.hash == hash &&
             ((k = p.key) == key || (key != null && key.equals(k))))
             e = p;//節點key已經有值了,將第一個節點賦值給e
         //該鏈是紅黑樹
         else if (p instanceof TreeNode)
             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         //該鏈是鏈表
         else {
             //遍歷鏈表
             for (int binCount = 0; ; ++binCount) {
                 //先將e指向下一個節點,然后判斷e是否是鏈表中最后一個節點
                 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;
                 }
                 //key已經存在直接終止,此時e的值已經為 p.next
                 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)
                 //修改已經存在Key的節點的value
                 e.value = value;
             afterNodeAccess(e);
             //返回key的原始值
             return oldValue;
         }
     }
     ++modCount;//用作修改和新增快速失敗
     if (++size > threshold)//超過最大容量,進行擴容
         resize();
     afterNodeInsertion(evict);
     return null;
}
View Code

  ①、判斷鍵值對數組 table 是否為空或為null,否則執行resize()進行擴容;

  ②、根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;

  ③、判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這里的相同指的是hashCode以及equals;

  ④、判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

  ⑤、遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

  ⑥、插入成功后,判斷實際存在的鍵值對數量size是否超過了最大容量threshold,如果超過,進行擴容。

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 此處先判斷p.hash == hash是為了提高效率,僅通過(k = e.key) == key || key.equals(k)其實也可以進行判斷,但是equals方法相當耗時!如果兩個key的hash值不同,那么這兩個key肯定不相同,進行equals比較是扯淡的! 所以先通過p.hash == hash該條件,將桶中很多不符合的節點pass掉。然后對剩下的節點繼續判斷。

 

h& (length-1)運算等價於對length取模,也就是h%length,但是&比%具有更高的效率。

數組的長度按規定一定是2的冪。因此,數組的長度的二進制形式是:10000…000, 1后面有偶數個0。 那么,length - 1 的二進制形式就是01111.111, 0后面有偶數個1。

這看上去很簡單,其實比較有玄機的,我們舉個例子來說明:

假設數組長度分別為15和16,優化后的hash碼分別為8和9,那么&運算后的結果如下:

   h & (table.length-1)   hash       table.length-1

       8 & (15-1):           0100   &    1110  =    0100
       9 & (15-1):           0101   &    1110  =    0100

       ---------------------------------------------------

       8 & (16-1):           0100   &    1111   =    010
       9 & (16-1):           0101   &    1111   =    0101

從上面的例子中可以看出:當它們和15-1(1110)“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到數組中的同一個位置上形成鏈表,那么查詢的時候就需要遍歷這個鏈
表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度為15的時候,hash值會與15-1(1110)進行“與”,那么最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是 這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的幾率,減慢了查詢的效率!

當n為2次冪時,會滿足一個公式:(n - 1) & hash = hash % n,計算更加高效。

只有是2的冪數的數字經過n-1之后,二進制肯定是 ...11111111 這樣的格式,這種格式計算的位置的時候,完全是由產生的hash值類決定

奇數n-1為偶數,偶數2進制的結尾都是0,經過&運算末尾都是0,會 增加hash沖突。

擴容

擴容(resize),我們知道集合是由數組+鏈表+紅黑樹構成,向 HashMap 中插入元素時,如果HashMap 集合的元素已經大於了最大承載容量threshold(capacity * loadFactor),這里的threshold不是數組的最大長度。那么必須擴大數組的長度,Java中數組是無法自動擴容的,我們采用的方法是用一個更大的數組代替這個小的數組

final Node<K,V>[] resize() {
        //將原始數組數據緩存起來
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//原數組如果為null,則長度賦值0
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果原數組長度大於0
            if (oldCap >= MAXIMUM_CAPACITY) {//數組大小如果已經大於等於最大值(2^30)
                threshold = Integer.MAX_VALUE;//修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了
                return oldTab;
            }
            //原數組長度擴大1倍(此時將原數組擴大一倍后的值賦給newCap)也小於2^30次方,並且原數組長度大於等於初始化長度16
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 閥值擴大1倍
            //如果原數組長度擴大一倍后大於MAXIMUM_CAPACITY后,newThr還是0
        }
        else if (oldThr > 0) 
            //舊容量為0,舊閥值大於0,則將新容量直接等於就閥值 
            //在第一次帶參數初始化時候會有這種情況
            //newThr在面算
            newCap = oldThr;
        else {
            //閥值等於0,oldCap也等於0(集合未進行初始化)
            //在默認無參數初始化會有這種情況 
            newCap = DEFAULT_INITIAL_CAPACITY;//數組長度初始化為16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//閥值等於16*0.75=12
        }
        //計算新的閥值上限
        //此時就是上面原數組長度擴大一倍后大於MAXIMUM_CAPACITY和舊容量為0、舊閥值大於0的情況
        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"})
            //創建容器大小為newCap的新數組
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        //將新數組賦給table
        table = newTab;
        //如果是第一次,擴容的時候,也就是原來沒有元素,下面的代碼不會運行,如果原來有元素,則要將原來的元素,進行放到新擴容的里面
        if (oldTab != null) {
            //把每個bucket都移動到新的buckets中
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;//元數據j位置置為null,便於垃圾回收
                    if (e.next == null)//數組沒有下一個引用(不是鏈表)
                        //直接將e的key的hash與新容量重新計算下標,新下標的元素為e
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)//紅黑樹
                        ((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) {
                                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;
    }
View Code

if ((e.hash & oldCap) == 0)如果判斷成立,那么該元素的地址在新的數組中就不會改變。因為oldCap的最高位的1,在e.hash對應的位上為0,所以擴容后得到的地址是一樣的,位置不會改變 ,在后面的代碼的執行中會放到loHead中去,最后賦值給newTab[j];如果判斷不成立,那么該元素的地址變為 原下標位置+oldCap,也就是lodCap最高位的1,在e.hash對應的位置上也為1,所以擴容后的地址改變了,在后面的代碼中會放到hiHead中,最后賦值給newTab[j + oldCap] 舉個栗子來說一下上面的兩種情況:
設:oldCap=16 二進制為:0001 0000
oldCap-1=15 二進制為:0000 1111
e1.hash=10 二進制為:0000 1010
e2.hash=26 二進制為:0101 1010
e1在擴容前的位置為:e1.hash & oldCap-1 結果為:0000 1010
e2在擴容前的位置為:e2.hash & oldCap-1 結果為:0000 1010
結果相同,所以e1和e2在擴容前在同一個鏈表上,這是擴容之前的狀態。 現在擴容后,需要重新計算元素的位置,在擴容前的鏈表中計算地址的方式為e.hash & oldCap-1 那么在擴容后應該也這么計算呀,擴容后的容量為oldCap*2=32 0010 0000 newCap=32,新的計算 方式應該為
e1.hash & newCap-1
即:0000 1010 & 0001 1111
結果為0000 1010與擴容前的位置完全一樣。
e2.hash & newCap-1
即:0101 1010 & 0001 1111
結果為0001 1010,為擴容前位置+oldCap。
而這里卻沒有e.hash & newCap-1
而是 e.hash & oldCap,其實這兩個是等效的,都是判斷倒數第五位是0,還是1。如果是0,則位置不變,是1則位置改變為擴容前位置+oldCap。

查找元素

①、get(Object key)

通過 key 查找 value:首先通過 key 找到計算索引,找到桶元素的位置,先檢查第一個節點,如果是則返回,如果不是,則遍歷其后面的鏈表或者紅黑樹。其余情況全部返回 null。

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 ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        //根據key計算的索引檢查第一個索引
        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;
}
View Code

②、判斷是否存在給定的 key 或者 value

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}
public boolean containsValue(Object value) {
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
        //遍歷數組
        for (int i = 0; i < tab.length; ++i) {
            //遍歷數組中的每個節點元素
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}
View Code

刪除元素

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

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;
    //(n - 1) & hash找到桶的位置
    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;
    //如果鍵的值與鏈表第一個節點相等,則將 node 指向該節點
    if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    node = p;
    //如果桶節點存在下一個節點
    else if ((e = p.next) != null) {
        //節點為紅黑樹
    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;
             //找到就停止,如果此時是第一次遍歷就找到,則node指向鏈表中第二個元素,p還是第一個元素
             //第一次沒找到,第二次找到,則node指向鏈表中第三個元素,p指向第二個元素,p是找到元素節點的父節點
             //所以需要遍歷的時候p和node 是不相等的,只有鏈表第一個元素就判斷相等時,p和node 相等
             break;
         }
         //第一次遍歷沒找到, 此時p指向第二個元素
         p = e;
     } while ((e = e.next) != null);
    }
    }
    //刪除節點,並進行調節紅黑樹平衡
    if (node != null && (!matchValue || (v = node.value) == value ||
                  (value != null && value.equals(v)))) {
    if (node instanceof TreeNode)
     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
    else if (node == p)
     //如果鍵的值與鏈表第一個節點相等,則將元素位置指向 node的下一個節點(鏈表的第二個節點),有可能node.next 為null
     tab[index] = node.next;
    else
     //如果鍵的值與鏈表第一個節點不相等,node的父節點的next指向node的next
     p.next = node.next;
    ++modCount;
    --size;
    afterNodeRemoval(node);
    return node;
    }
    }
    return null;
}
View Code

遍歷元素

HashMap<String, String> map = new HashMap<>();
map.put("1", "A");
map.put("2", "B");
map.put("3", "C");
map.put("4", "D");
map.put("5", "E");
map.put("6", "F");
for(String str : map.keySet()){
    System.out.print(map.get(str)+" ");
}

for(HashMap.Entry entry : map.entrySet()){
    System.out.print(entry.getKey()+" "+entry.getValue());
}
View Code

 重寫equals方法需同時重寫hashCode方法

各種資料上都會提到,“重寫equals時也要同時覆蓋hashcode”,我們舉個小例子來看看,如果重寫了equals而不重寫hashcode會發生什么樣的問題

/**
 * Created by chenhao on 2018/9/28.
 */
public class MyTest {
    private static class Person{
        int idCard;
        String name;

        public Person(int idCard, String name) {
            this.idCard = idCard;
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Person person = (Person) o;
            //兩個對象是否等值,通過idCard來確定
            return this.idCard == person.idCard;
        }

    }
    public static void main(String []args){
        HashMap<Person,String> map = new HashMap<Person, String>();
        Person person = new Person(123,"喬峰");
        //put到hashmap中去
        map.put(person,"天龍八部");
        //get取出,從邏輯上講應該能輸出“天龍八部”
        System.out.println("結果:"+map.get(new Person(123,"蕭峰")));
    }
}
View Code

如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。盡管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,所以put操作時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過key取出value的時候 key(hashcode2)-->hash-->indexFor-->最終索引位置,由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)

再想象一下,假如兩個Java對象A和B,A和B相等(eqauls結果為true),但A和B的哈希碼不同,則A和B存入HashMap時的哈希碼計算得到的HashMap內部數組位置索引可能不同,那么A和B很有可能允許同時存入HashMap,顯然相等/相同的元素是不允許同時存入HashMap,HashMap不允許存放重復元素。

  所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希沖突,應盡量避免)。

 

 


免責聲明!

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



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