1. 程式人生 > >HashMap和Hashtable理解與對比

HashMap和Hashtable理解與對比

一、概述

HashMap和Hashtable的區別在面試的時候經常會被問到,那麼它們有什麼區別呢?這裡談一下它們各自的特點以及它們的區別在哪裡。

二、HashMap

1、HashMap是鍵值對key-value形式雙列集合。它的底層儲存原理是雜湊表。為了簡明描述雜湊表(陣列+連結串列),我畫了一個圖(不專業,輕噴)。

2、對應HashMap採用雜湊表儲存鍵值對元素的方式, 配合著上圖做一些說明:

1)E*代表一個Node節點,每個Node節點就是我們理解的一個key-value的mapping對映。

2)每個Node除了儲存了key和value的對映外呢,還儲存了它下一Node的引用。例如圖中,Eb儲存了Ebb

的引用,而Ebb儲存了Ebbb的引用。

3)HashMap的put(key,value)方法介紹

每一個連結串列,如Eb-->Ebb-->Ebbb,這三個節點的key是不相等的。那麼你可能會問,為什麼它們三個會被放

在一個連結串列中呢?是這樣的。當你呼叫put(key,value)方法時,會根據key計算出一個hash值,然後再通

過這個hash值和map當前的長度計算出一個數值,然後將這個數值作為圖中陣列tab腳標而獲取這個腳標

對應的Node節點。如果這個節點不存在,則直接建立一個新的Node節點,插入到陣列中的你計算出的那個

腳標的位置。如果存在,則判斷key和你put進來的key是否相等(注意相等的判定:hash值相等且equal

相等),如果相等,那麼直接更新其值value,也就是我們常見的覆蓋舊value操作,如果舊value為null,則

直接將null值設定為你傳進來的value值,如果不相等(此處不介紹LinkedHashMap)則去以遍歷的方式尋

找這個節點的next節點,如果和這個連結串列的每個節點的next節點都不相等,則在連結串列的最後一個Node節點

後建立新節點。如果其中判定有一個相等,那麼進行覆蓋值並返回舊值操作。當然,這只是put方法的概要

解讀,更詳細的解讀需要自己看一下原始碼。

HashMap.put(key,value)方法的注意點:

(1)沒有synchronized關鍵字修飾,意味著它是非執行緒安全的。

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

 (2)key和value可以是null-null、null-value、key-null的形式。如果Map中不存在將要新增的元素那麼返

回值為null,如果已經存在且value不為null,那麼新value覆蓋舊value,返回舊value,如果舊value為null,

那麼將新value和key形成一個key-value對映,並返回null。key為null的時候,null也是一個鍵,鍵具有唯

一性,value可以重複。

3、HashMap的resize()方法理解

這個方法時HashMap容量不夠時進行擴容的方法,覺得有必要說一下。

當HashMap的容量不夠用時,再往容器中新增元素時,HashMap會進行擴容操作。當HashMap的容量為最大的

時,則不擴容,但是容器閾值會設定為Integer型別的最大值。當不是最大的時,容器會進行擴容,容量會變為

原來的二倍(此時也不大於最大容量),並且閾值也會隨之變化,變為原閾值的二倍。擴容就是在堆中新

建立一個HashMap容器,然後將原來就的HashMap中的元素放到新容器中,放置的時候會重新計算每個Node節

點在雜湊表中的位置。

4、HashMap的構造

我們常用的都是空參構造。空參構造一個HashMap,那麼它的構造方法使用的是預設的初始化容量16和

載入因子0.75。

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

 當我們使用帶參構造:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

此時,閾值(threshold )也不是簡單的容量*載入因子獲取,而是需要通過一個演算法,如下:

this.threshold = tableSizeFor(initialCapacity);

tableSizeFor(initialCapacity):

    /**
     * Returns a power of two size for the given target capacity.
     * 根據給定的目標容量,返回一個2的整數倍的數(自己翻譯,水平有限,輕噴)。
     */
    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;
    }

這裡探討這個構造的本意在於,如果我們能預估出Map大概能儲存多個少鍵值對,那麼我們可以直接通過指

定容量這種帶參構造穿件Map例項,這樣可以避免Map的擴容造成的資源和效能浪費。對應載入因子0.75的

預設值,我們一般是不做修改的,這涉及到Map的存取效能問題。

HashMap是非執行緒安全的,是因為HashMap的方法都是沒有用synchronized關鍵字修飾的。

三、Hashtable

Hashtable已經被棄用的一個類,效能比較低,它有一些自己的特點,不知道你發現沒有,它不符合大小駝

峰命名規則,這點很討厭。

1、Hashtable的方法幾乎都是同步的,都有synchronized關鍵字修飾,因此和HashMap相比,它是執行緒安全的。

public synchronized V put(K key, V value) {...}

2、Hashtable中key-value的對映,key和value 都是不允許為null的,如果為null了呢?對不起,空指標異常丟擲。

3、Hashtable在計算節點元素在雜湊表中的位置使用的演算法稍有區別,它有它的好處,但和HashMap的演算法比

起來明顯效能低一些。

4、Hashtable的擴容是原來容量的二倍加1(2n+1),原始碼參考:int newCapacity = (oldCapacity << 1) + 1;

四、HashMap和Hashtable有哪些主要區別呢?

1、HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類。不過它們都實現了同時實現了map、Cloneable(可複製)、Serializable(可序列化)這三個介面。

2、Hashtable比HashMap多提供了elments() 和contains() 兩個方法。

3、HashMap的key-value支援key-value,null-null,key-null,null-value四種。而Hashtable只支援key-value一種(即

key和value都不為null這種形式)。既然HashMap支援帶有null的形式,那麼在HashMap中不能由get()方法來判斷

HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷,因為使用get的時候,當返回null時,你無法判斷到底

是不存在這個key,還是這個key就是null,還是key存在但value是null。

4、執行緒安全性不同,HashMap的方法都沒有使用synchronized關鍵字修飾,都是非執行緒安全的,而Hashtable的方法幾乎

都是被synchronized關鍵字修飾的。但是,當我們需要HashMap是執行緒安全的時,怎麼辦呢?我們可以通過Collections.synchronizedMap(hashMap)來進行處理,亦或者我們使用執行緒安全的ConcurrentHashMap。ConcurrentHashMap雖然也是執行緒安全的,但是它的效率比Hashtable要高好多倍。因為ConcurrentHashMap使用了分段鎖,並不對整個資料進行鎖定。

5、初始容量大小和每次擴充容量大小的不同 
Hashtable預設的初始大小為11,之後每次擴充,容量變為原來的2n+1。HashMap預設的初始化大小為16。之後每次擴充,容量變為原來的2倍。

6、計算hash值的方法不同 
為了得到元素的位置,首先需要根據元素的 KEY計算出一個hash值,然後再用這個hash值來計算得到最終的位置。

Hashtable直接使用物件的hashCode。hashCode是JDK根據物件的地址或者字串或者數字算出來的int型別的數值。然後再使用除留餘數發來獲得最終的位置。 
這裡寫圖片描述

Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。 
HashMap為了提高計算效率,將雜湊表的大小固定為了2的冪,這樣在取模預算時,不需要做除法,只需要做位運算。位運算比除法的效率要高很多。

HashMap的效率雖然提高了,但是hash衝突卻也增加了。因為它得出的hash值的低位相同的概率比較高,而計算位運算

為了解決這個問題,HashMap重新根據hashcode計算hash值後,又對hash值做了一些運算來打散資料。使得取得的位置更加分散,從而減少了hash衝突。當然了,為了高效,HashMap只做了一些簡單的位處理。從而不至於把使用2 的冪次方帶來的效率提升給抵消掉。

這裡寫圖片描述

 

本文參考了以下內容:https://blog.csdn.net/wangxing233/article/details/79452946