Java HashMap實現原理 源碼剖析,javahashmap

來源:互聯網
上載者:User

Java HashMap實現原理 源碼剖析,javahashmap

HashMap是基於雜湊表的Map介面實現,提供了所有可選的映射操作,並允許使用null值和null建,不同步且不保證映射順序。下面記錄一下研究HashMap實現原理。

HashMap內部儲存

在HashMap內部,通過維護一個 瞬時變數數組table (又稱:桶) 來儲存所有的索引值對關係,桶 是個Entry對象數組,桶 的大小可以按需調整大小,長度必須是2的次冪。如下代碼:

   /**     * 一個空的entry數組,桶 的預設值     */    static final Entry<?,?>[] EMPTY_TABLE = {};    /**     * 桶,按需調整大小,但必須是2的次冪     */    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

初始容量與負載因子

HashMap有兩個參數影響效能,初始容量和負載因子。容量是雜湊表中 桶 的數量,初始容量只是雜湊表在建立時的容量,負載因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中條目數超出了負載因子與當前容量的乘積時,則要對該Hash表進行rehash操作(即重建內部資料結構),重建時以當前容量的兩倍數目建立。可以通過構造器設定初始容量與負載因子,預設初始容量是16個條目,最大容量是2^30次方個條目,預設負載因子是0.75

桶 就像一個存水的水桶,它預設的初始存水容量是16個單位的水,預設在灌水灌到16*0.75時,在下次添加資料時會先擴充容量,擴充到32單位。0.75就是負載因子,初始容量與負載因子可以通過建立水桶的時候進行設定。水桶最大的容量是2的30次方個單位的水。當初始容量設定的數量大於最大容量時,以最大容量為準。當擴充時如果大於等於最大容量時則直接返回。

如下為HashMap的部分源碼,定義了預設初始容量、負載因子及其他一些常量:

/**     * 預設初始化容量,必須為2的次冪The default initial capacity - MUST be a power of two.     */    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16    /**     * 最大容量,如果通過建構函式參數中傳遞初始化容量大於該最大容量了,也會使用該容量為初始化容量
     * 必須是2的次冪且小於等於2的30次方
     */    static final int MAXIMUM_CAPACITY = 1 << 30;    /**     * 預設的負載因子,可以通過建構函式指定     */    static final float DEFAULT_LOAD_FACTOR = 0.75f;    /**     * 一個空的數組表,當 桶沒有初始化的時候     */    static final Entry<?,?>[] EMPTY_TABLE = {};    /**     * 桶 , 儲存所有的索引值對條目,可以按需調整大小,長度大小必須為2的次冪      */    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;    /**     * Map中索引值對的數量,在每次新增或刪除的時候都會對size進行+1或者-1操作.     */    transient int size;    /**     * 負載值,需要調整大小的臨界值,為:(capacity * load factor).在每次調整大小後會使用新的容量計算一下     * @serial     */    // If table == EMPTY_TABLE then this is the initial capacity at which the    // table will be created when inflated.    int threshold;    /**     * 負載因子,如果建構函式中沒有指定,則採用預設的負載因子,     *     * @serial     */    final float loadFactor;    /**     * HashMap結構修改次數,結構修改時改變HashMap中的映射數量或修改其內部結構(例如,* rehash方法,重建內部資料結構),此欄位用於在
     * HashMap的集合視圖上產生的迭代器都處理成快速失敗的     */    transient int modCount;

 

初始容量與負載因子效能調整

通常,預設負載因子(0.75)在時間和空間成本上尋求一種折中。負載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數HashMap類的操作中,包括get和put操作,都反映了這一點)。在設定初始容量時應該考慮到映射中所需的條目數及其負載因子,以便最大限度的減少rehash操作次數。如果初始容量大於最大條目數除以載入因子,則不會發生rehash操作。

如果很多映射關係要儲存在HashMap執行個體中,則相對於按需執行自動的rehash操作以增大表的容量來說,使用足夠大的初始容量建立它將使得映射關係能更有效儲存。

如下為重建HashMap資料結構的代碼:

void resize(int newCapacity) {        Entry[] oldTable = table;        int oldCapacity = oldTable.length;        if (oldCapacity == MAXIMUM_CAPACITY) { // 如果容量已達最大限制,則設定下負載值後直接返回            threshold = Integer.MAX_VALUE;            return;        }        // 建立新的table儲存資料        Entry[] newTable = new Entry[newCapacity];        // 將舊table中的資料轉存到新table中去,這一步會花費比較多的時間        transfer(newTable, initHashSeedAsNeeded(newCapacity));        table = newTable;        // 最後設定下下次調整大小的負載值        threshold = (int) Math.min(newCapacity * loadFactor,                MAXIMUM_CAPACITY + 1);}

 

HashMap構造方法

第四個構造方法是以已經存在的Map建立一個新的HashMap,稍後再說,前三個構造方法,其實最終調用的都是第三個帶兩個參數的方法,如果沒有傳遞參數則使用預設的數值,代碼如下:

    /**     * Constructs an empty <tt>HashMap</tt> with the default initial capacity     * (16) and the default load factor (0.75).     */    public HashMap() {        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);    }    /**     * Constructs an empty <tt>HashMap</tt> with the specified initial     * capacity and the default load factor (0.75).     *     * @param  initialCapacity the initial capacity.     * @throws IllegalArgumentException if the initial capacity is negative.     */    public HashMap(int initialCapacity) {        this(initialCapacity, DEFAULT_LOAD_FACTOR);    }    /**     * 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;        threshold = initialCapacity;        init();    }

由上可以看出,在建構函式中,如果初始容量給的大於最大容量,則直接以最大容量代替。

put方法

接下來就看看HashMap中比較重要的部分

    /**     * 在此映射中關聯指定值與指定建。如果該映射以前包含了一個該鍵的映射關係,則舊值被替換      *     * @param 指定將要關聯的鍵      * @param 指定將要關聯的值      * @return 與key關聯的舊值,如果key沒有任何映射關係,則返回null(返回null還可能表示該映射之前將null與key關聯)      */    public V put(K key, V value) {        if (table == EMPTY_TABLE) {            inflateTable(threshold);        }        if (key == null)            return putForNullKey(value);        int hash = hash(key);        int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {            Object k;            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {                V oldValue = e.value;                e.value = value;                e.recordAccess(this);                return oldValue;            }        }        modCount++;        addEntry(hash, key, value, i);        return null;    }

因為新增條目的時候,需要計算hash值,長度不夠時需要調整長度,當計算的儲存位置已有元素的時候需要進行鏈表式的儲存,所以使用HashMap新增操作的效率並不是太高。

get方法

首先看下get方法的源碼:

    /**     * 返回指定鍵所映射的值;如果對於該鍵來說,此映射不包含任何映射關係,則返回null     * 返回null值並不一定表明該映射不包含該鍵的映射,也可能改映射將該鍵顯示的映射為null,可使用containsKey操作來區分這兩種情況      * @see #put(Object, Object)     */    public V get(Object key) {        if (key == null)            return getForNullKey();        Entry<K,V> entry = getEntry(key);        return null == entry ? null : entry.getValue();    }    final Entry<K,V> getEntry(Object key) {        if (size == 0) {            return null;        }        int hash = (key == null) ? 0 : hash(key);        for (Entry<K,V> e = table[indexFor(hash, table.length)];             e != null;             e = e.next) {            Object k;            if (e.hash == hash &&                ((k = e.key) == key || (key != null && key.equals(k))))                return e;        }        return null;}

get方法實現較簡單,以下是幾個步驟:

通過查看get的源碼可以發現,get方法通過key的雜湊值與桶的長度計算儲存位置,基本上一下就能定位到要找的元素,即使再遍曆幾個重複雜湊值的key,也是很快速的,因為雜湊值相對唯一,所以HashMap對於尋找效能是非常快的。

自訂對象作為HashMap的鍵

class User {    // 社會安全號碼碼    protected int idNumber;        public User(int id){        idNumber = id;    }}public class TestUser{    public static void main(String[] args) {        Map<User, String> map = new HashMap<User, String>();        for (int i=0; i<5; i++) {            map.put(new User(i), "姓名: " + i);        }        System.out.println("User3 的姓名:" + map.get(new User(3)));    }}
輸出:User3 的姓名:null

如上代碼,通過自訂的User類執行個體作為HashMap的對象時,在列印的時候是無法找到User3的姓名的,因為User類自動繼承基類Object,所以這裡會自動使用Object的hashCode方法產生雜湊值,而它預設是使用對象的地址計算雜湊值的。因此new User(3)產生的第一個執行個體的雜湊值與產生的第二個執行個體的雜湊值是不一樣的。但是如果只需要簡單的覆蓋hashCode方法,也是無法正常運作的,除非同時覆蓋equals方法,它也是Object的一部分。HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同,可以參考上面的get或put方法。

正確equals()方法必須滿足下列5個條件:---參考《Java編程思想》—489頁

再次強調:預設的Object.equals()只是比較對象的地址,所以一個new User(3)並不等於另一個new User(3)。因此,如果要使用自己的類作為HashMap的鍵,必須同時重載hashCode()和equals().

如下代碼可以正常運作:

class User {    // 社會安全號碼碼    protected int idNumber;        public User(int id){        idNumber = id;    }    @Override    public int hashCode() {        return idNumber;    }    @Override    public boolean equals(Object obj) {        return obj instanceof User && (idNumber==((User)obj).idNumber);    }        }public class TestUser{    public static void main(String[] args) {        Map<User, String> map = new HashMap<User, String>();        for (int i=0; i<5; i++) {            map.put(new User(i), "姓名: " + i);        }        System.out.println("User3 的姓名:" + map.get(new User(3)));    }}輸出:User3 的姓名:姓名: 3

上面只是簡單的在hashCode中返回了idNumber作為唯一的判別,使用者也可以根據自己的業務實現自己的方法。在equals方法中,instanceof會悄悄的檢查對象是否為null,如果instanceof左邊的參數為null,則會返回false,如果equals()的參數不為null且類型正確,則基於每個對象中的實際的idNumber進行比較。從輸出可以看出,現在的方式是正確的。

 

參考:

   《Java編程思想》

    JDK API協助文檔

    JDK源碼

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.