HashMap詳細解析,高頻面試題解答,源碼閱讀

HashMap 詳解

下面我首先拋出以下問題,讓我們帶着這些問題開始解析 HashMap:

  1. JDK8 中 HashMap 有哪些改動?
  2. JDK8 爲什麼要使用紅黑樹?
  3. 爲什麼重寫對象的 Equals 方法,要重寫 HashCode方法,跟 HashMap有關係嗎?
  4. HashMap 是線程安全的嗎? 遇到過 ConcurrentModificationException 異常嗎?爲什麼會出現,如何解決?
  5. 在使用 HashMap 過程中,我們應該注意什麼?
  6. 手寫HashMap?
  7. 你知道 HashMap 的工作原理嗎?
  8. HashMap 中能 PUT 兩個相同的 key 嗎?爲什麼能或爲什麼不能?
  9. HashMap 中的鍵值可以爲 NULL?能簡單說一下原理嗎?
  10. HashMap 擴容機制是怎麼樣的, JDK 7與 JDK 8 有什麼不同嗎?

原先是打算寫一篇介紹,但是發現了一篇博客寫的非常棒,我就不重複造輪子了。給出鏈接Java 8系列之重新認識HashMap

結合博客,再給出文章開始給出的問題的解答

JDK 8 中 HashMap 有哪些改動?

HashMap 在原有的基礎上增加了*紅黑樹 *結構,當鏈表長度大於8會轉換爲紅黑樹進行處理,當結點數目小於 6 則會還原成鏈表。

JDK 8 爲什麼要使用紅黑樹?

首先紅黑樹遍歷的時間複雜度爲 O(logn),長度爲8 時,平均查找長度是 3 ,而此時鏈表的平均查找長度是 4,所以轉換爲樹在遍歷、查找時性能會得到提升。
不過這個答案並不嚴謹,選擇數字 8 的原因如下文所述。

0: 0.60653066 
1: 0.30326533 
2: 0.07581633 
3: 0.01263606 
4: 0.00157952 
5: 0.00015795 
6: 0.00001316 
7: 0.00000094 
8: 0.00000006 

理想情況下使用隨機的哈希碼,容器中節點分佈在hash桶中的頻率遵循泊松分佈,具體可以查看泊松分佈,按照泊松分佈的計算公式計算出了桶中元素個數和概率的對照表,可以看到鏈表中元素個數爲8時的概率已經非常小,再多的就更少了,所以原作者在選擇鏈表元素個數時選擇了8,是根據概率統計而選擇的。

爲什麼重寫對象的 Equals 方法,要重寫 HashCode方法,跟 HashMap有關係嗎?

Equals 方法是用於比較對象是否相等,通過源碼可以得知, HashMap 中結點的比較是根據 key 和 value 是否同時相等判斷的。

 public final boolean equals(Object o) {
     if (o == this)
     return true;
     if (o instanceof Person) {
         Person e = (Person)o;
         if (Objects.equals(id, e.getId()) &&
         Objects.equals(name, e.getName()))
         return true;
     }
     return false;
 }

假設我們現在只重寫 Equals 方法,而不重寫 HashCode方法

 public final boolean equals(Object o) {
     if (o == this)
     return true;
     if (o instanceof Person) {
         Person e = (Person)o;
         if (Objects.equals(id, e.getId()))
         return true;
     }
     return false;
 }

那麼這種情況下,按照代碼而言應該是,只要 Person 對象的 id 相等,則默認他們是相等的,但是當我們這麼操作

Person p1 = new Person("1", "zhangsan");
Person p2 = new Person("2", "lisi");
// 此時 p1 和 p2 是相等的
System.out.println(p1.equals(p2));
HashMap map = new HashMap();
map.put(p1, "test");
System.out.println(map.get(p2));

按照道理來說這裏應該是可以獲取到 test ,然而事實上返回的是 null,因爲在 HashMap 在進行 put() 和 get() 取出時都是通過 hash() 方法來獲取對象的 hash 值,確定數組的下標。當重寫了 hash 方法就可以確定 hash 值相同。

HashMap 是線程安全的嗎? 遇到過 ConcurrentModificationException 異常嗎?爲什麼會出現,如何解決?

不是線程安全的,需要線程安全可以採用 ConcurrentHashMap 。
ConcurrentModificationException 異常時由於 modCount 和 exceptModCount 不同,這裏使用的是 fastfial 機制,出現錯誤直接拋出異常。而 iterater 中的 remove() 方法則可以保證不出現異常。

在使用 HashMap 過程中,我們應該注意什麼?

使用HashMap時,要注意HashMap容量和加載因子的關係,這將直接影響到HashMap的性能問題。加載因子過小,會提高HashMap的查找效率,但同時也消耗了大量的內存空間,加載因子過大,節省了空間,但是會導致HashMap的查找效率降低。

如果能夠確定大小,在初始化的時候一定要給出,避免不必要的擴容操作。

手寫HashMap?

public class MyHashMap<K,V> {

    // 數組的長度    默認數組長度是 16
    private static final int length = 8;
    // HashMap 的大下
    private int size;
    private Entry<K, V>[] table;

    // 1. 實現構造方法
    public MyHashMap() {
        table = new Entry[length];
    }

    public V put(K key, V value) {
        int hash = key.hashCode();
        int index = hash % length;
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            // 如果存在相同的 key 直接將原先的覆蓋
            if (entry.key.equals(key)) {
                V oldValue = entry.value;
                entry.value = value;
                return oldValue;
            }
            entry = entry.next;
        }
        // 結點中並沒有
        addEntry(key, value, index);
        return value;
    }

    private void addEntry(K key, V value, int index) {
        size++;
        table[index] = new Entry<>(key, value, table[index]);
    }

    public int size() {
        return size;
    }

    public V get(K key) {
        int hash = key.hashCode();
        int index = hash % length;
        if (table[index] == null) {
            return null;
        }
        for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
            if (entry.key.equals(key)) {
                return entry.value;
            }
        }
        return null;
    }

    public void clear() {
        // 不爲空則全部賦給 null, 將 size 修改爲 o
        Entry<K, V>[] newTable;
        if ((newTable = table) != null && size > 0) {
            size = 0;
            for (int i = 0; i < newTable.length; ++i) {
                newTable[i] = null;
            }
        }
    }

    class Entry<K, V>{
        private K key;
        private V value;
        private Entry<K, V> next;

        public Entry(K key, V value, Entry<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
}

你知道 HashMap 的工作原理嗎?

這個題目相對比較寬泛,怎麼回答都可以。主要考察的是個人對於 HashMap 掌握的深度。

HashMap 中能 PUT 兩個相同的 key 嗎?爲什麼能或爲什麼不能?

可以,put 兩個相同的 key 的時候,會將之前 put 的值替換掉。

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;

HashMap 中的鍵值可以爲 NULL?能簡單說一下原理嗎?

key 是可以爲 null 的,HashMap 插入 值時會先計算 key 的 hash 值,我們可以看到當 key 爲 null 時會返回 0, 也就是說位於數組的下標 0 處。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap 擴容機制是怎麼樣的, JDK 7與 JDK 8 有什麼不同嗎?

這個問題我也理解的不是很清楚,有興趣的可以好好研究一下給出的參考博客。

參考鏈接:
Java 8系列之重新認識HashMap

HashMap, ConcurrentHashMap 原理及源碼,一次性講清楚!

高併發編程:HashMap 深入解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章