1. 程式人生 > >JDK1.8原始碼(三)——java.util.HashMap

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掉。然後對剩下的節點繼續判斷。

擴容

擴容(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方法中有提到。)

  所以,在重寫equals的方法的時候,必須注意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個物件,呼叫hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個物件,其hashCode可以相同(只不過會發生雜湊衝突,應儘量避免)。