程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java HashMap完成原理 源碼分析

Java HashMap完成原理 源碼分析

編輯:關於JAVA

Java HashMap完成原理 源碼分析。本站提示廣大學習愛好者:(Java HashMap完成原理 源碼分析)文章只能為提供參考,不一定能成為您想要的結果。以下是Java HashMap完成原理 源碼分析正文


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結構辦法

image

第四個結構辦法是以曾經存在的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;
    }
  1. 首先put辦法中,先判別 桶 能否為默許的未初始化形態,假如未初始化則調用 inflateTable 辦法去初始化,然後判別參數key能否為null,假如為null,則調用putForNullKey專門停止放key為null的數據,putForNullKey辦法與上面的第3步開端其實都是一樣的,只不過key為null的數據默許存儲地位就是第一個,即下標默許為0。
  2. 假如key不是null,則調用hash()辦法獲取key的hash值,可以依據hash值、桶的長度經過indexFor辦法計算該key可以放到桶的地位。
  3. Entry對象中有一個屬性next,可以構成一個單向鏈表,用來存儲哈希值相反的元素。因而當計算出來key的hash值反復時,存儲地位也會反復,只需判別一下存儲地位的元素及該元素的next屬性鏈表中能否與給定的key和key的hash值能否完全分歧就可以了。假如有完全分歧的,代表曾經存在,則交換舊值,並把舊值做為前往值直接前往。
  4. 把構造修正次數自增1
  5. 調用addEntry辦法將新的鍵值對添加到HashMap中。addEntity辦法首先判別以後條目數據能否曾經大於等於負載值(桶的容量*負載因子)且桶的指定地位不為null,假如曾經大於且指定地位不為null,則調調整桶的容量為以後容量的2倍,調整桶的容量參照下面的初始容量與負載因子功能調整 目錄。重新計算Hash值,計算寄存地位。調用createEntry辦法寄存到 桶 中
        void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
            createEntry(hash, key, value, bucketIndex);
        }
    
        void createEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
        }
    
       /**
        * Entry結構辦法,創立一個新的Entry.
        */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
  6. 在 createEntry 辦法中,首先獲取指定地位的entry,然後重生成一個entry,在生成entry時把原有的entry存儲到重生成的entry的next屬性中(參考Entry的結構辦法),並把指定地位的entry交換成重生成的。

由於新增條目的時分,需求計算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辦法完成較復雜,以下是幾個步驟:

  1. 首先判別key能否為null,假如為null,則調用 getForNullKey 辦法來獲取,假如不為null則調用 getEntry 辦法來獲取。getForNullKey辦法與getEntity根本上分歧,只不過少了一個步驟,就是默許的key為null的存儲地位在第一個,即下標為0,沒有去計算地位而已。
  2. getEntity辦法依據key計算哈希值,然後用哈希值、桶的長度計算存儲地位。
  3. getEntity以獲取指定地位的entry作為遍歷的開端,遍歷entry的next單鏈表,假如entry的哈希值與計算的哈希值分歧且entry的key與指定的相等則前往entry
  4. 依據getEntity前往的值,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頁

  1. 自反性。對恣意x,x.equals(x)一定前往true
  2. 對稱性。對恣意x和y,假如有y.equals(x)前往true,則x.equals(y)也前往true
  3. 傳遞性。對恣意x,y,z,假如有x.equals(y)前往true,y.equals(z)前往true,則x.equals(z)一定前往true
  4. 分歧性,對恣意x和y,假如對象中用於等價比擬的信息沒有改動,那麼無論調用x.equals(y)多少次,前往的後果應該堅持分歧,要麼分歧是true,要麼分歧是false.
  5. 對任何不是null的x,x.equals(null)一定前往false

再次強調:默許的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源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved