1. 程式人生 > >第一章 JAVA集合之HashMap原始碼淺析

第一章 JAVA集合之HashMap原始碼淺析

              屌絲程式設計師的奮鬥之路現在開始

               java集合這一塊無論在面試或在寫程式碼中,我們都會接觸到,所以java集合是特別重要的,其中HashMap更是被我們經常用到。

 一.概括

               HashMap是用鍵值對的既已key-value的形式來儲存值的,當然這只是展現給大家的一種表象,key和value都可以為空,但是key不能重複,HashMap不是現線安全的,如果想讓HashMap變成現線安全的,可以呼叫Collections的靜態方法synchronized方法。其實HashMap是用一個動態陣列和多個連結串列來存放key-value的

,key-value不是直接放在陣列和連結串列裡面的,key-value是被一個叫Entry的物件給封裝了,所以動態資料和連結串列裡面是存放的Entry物件的。

二.HashMap的資料結構

               HashMap可以說是由一個動態陣列和多個連結串列組成,連結串列是接在每一個數組單元下面的,動態陣列和連結串列中儲存的單元是一個叫Entry的物件,從下面的圖中可以很直觀的看出HashMap的資料結構,其中每一個單元格儲存的就是Entry物件了,這一個Entry物件是HashMap的一個靜態類

             

            Entry原始碼          

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;//指向一下個Entry物件,他是為解決hash衝突而存在的。
        int hash;

        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

        從Entry的屬性中看到了我們所熟悉的key和value,沒錯,這就是我們在用HashMap的時候所要接觸到的key,value,Entry對key-value進行了封裝,我們再看看Enrty的next屬性,儲存的就是指向下一個物件的指標,當然java是沒有指標這一說的,我覺得在這裡將它當成指標更好理解,next在出現hash衝突的時候會發生作用,現在我們再看看上面的那一張圖,現在知道為什麼那些綠色的連結串列是怎麼連線起來的了吧,就是通過Entry的next屬性指向下一個Entry物件連線起來的,所以在HashMap原始碼中是看不到動態連結串列的定義,但是它確實是存在的。

三.HashMap的API

     1.HashMap的相關屬性

    /**
     * HashMap中陣列的預設大小是16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 陣列的最大長度
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 預設的載入因子是0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 存放Entry物件的陣列,也是HashMap存放資料的地方
     */
    transient Entry<K,V>[] table;

    /**
     * HashMap的存入值得個數,注意:他和陣列的大小是沒有關係的
     */
    transient int size;

    /**
     * 邊界值  <span style="font-family: Arial, Helvetica, sans-serif;">邊界值=HahsMap的容量*載入因子</span>
     * @serial
     */
    int threshold;

    /**
     *載入因子
     * @serial
     */
    final float loadFactor;
            
        邊界值=陣列大小*載入因子

當HashMap所儲存物件的個數超過邊界值的時候就會對陣列進行擴容,例如HashMap預設的載入因子是0.75,陣列預設的大小是16,所以邊界值是12,當我們在HashMap中儲存的值大於等於12的時候,HashMap會對陣列table進行2倍的擴容。
       

    2.HashMap的構造方法

/**
 *給陣列設定初始容量和載入因子
 */
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);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
		/*
		 *將陣列的容量設定為大於初始容量的最小2次冪
		 *例如你給HashMap設定的初始容量是20,那HashMap會自動將容量變為32
		 */
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
    }

    /**
     *如果只設置HashMap初始大小,就用預設的載入因子:0.75
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     *給HashMap設定成預設的大小:16,預設的載入因子0.75
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     *將Map集合存入HashMap
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }
需要注意的地方是當我們用 HashMap(int initialCapacity, float loadFactor)進行初始化的時候,HashMap裡面陣列的大小不是我們設定的initialCapacity值,而是大於initialCapacity的最小2次冪。

     3.HahMap的hash算

          看的不是太懂,需要知道HahMap就是根據key值來進行hash計算的

        /**
	  *HashMap的hash演算法
	  */
	 final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

     4.HahMap的取值方法:get(Object key)         

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
我們先看看getEntry這個方法
final Entry<K,V> getEntry(Object key) {
		//對key進行hash計算得到hash值
        int hash = (key == null) ? 0 : hash(key);
		//再用hash值對資料長隊進行取模運算得到key在陣列的儲存位置,再遍歷以陣列這個位置為頭結點的連結串列
        for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {
            Object k;
			//先去比較key的hash值是否相等,相等再去比較key值是否相等,如果兩個都相等,才算找到了
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
這裡在比較key值是否相等的時候,前面為什麼還要比較hash值是否相等,我覺得是用hash值比較更加快速,能快速的排除不相等的物件。

再看看getForNullKey這個特殊的方法

private V getForNullKey() {
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
可以看到是直接就定位到了table[0]這個地方,說明當我們在儲存key=null的鍵值對的時候,HashMap是直接放在table[0]這個連結串列中的

     5.HahMap的存值方法:V put(K key, V value)

                先用圖來說明put方法的大體過程,再看原始碼

put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。如下:

        首先我們假設一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:


        然後我們插入一個數:put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數,程式對該“連結串列”進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。


        在put(33,33),key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。


public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
		/*
		 *用陣列長度對key的hash值進行取模運算,得到key對應陣列的某一個位置
		 *再對以這個陣列元素為頭結點的連結串列進行遍歷
		 */
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			//如果HahMap中有key的存在,就將新的value替換舊的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
				//這個方法沒有做任何操作
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
		//將新新增的key—value放在table[i]的位置
        addEntry(hash, key, value, i);
        return null;
    }
我們先看indexFor方法,indexFor方法是如何利用陣列長度對hash值進行取模的
static int indexFor(int h, int length) {
        return h & (length-1);
    }
很簡單,對不對,但這裡面卻蘊含著大智慧,首先&運算是要比%這種運算要快很多的,還有這個length這個值始終是2的n次冪,我們前面講到了當在運用HashMap的構造方法的時候給table設定初始值,table的長度是大於這個初始值的最小n次冪,length-1一定是111...11這樣的二進位制,這樣就再對hash值取模的時候資料的每一個地方都是可以達到的。這樣就會在儲存值得時候減少hash衝突。

addEntry方法

void addEntry(int hash, K key, V value, int bucketIndex) {
	    //先比較size和邊界值的大小
        if ((size >= threshold) && (null != table[bucketIndex])) {
			//如果當size大於等於邊界值的時候,會對陣列進行2倍擴容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
			//從新計算key-value存放到陣列的地方
            bucketIndex = indexFor(hash, table.length);
        }
        //將新加入的key-value放入到陣列中
        createEntry(hash, key, value, bucketIndex);
    }
createEntry方法
//將新加入的key-value放到table的陣列中,再將新加入的Entry的next指向陣列原來的位置的值,這樣就形成了連結串列
	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++;
    }

resize方法,對陣列進行擴容
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        transfer(newTable, rehash);
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

transfer方法,從新計算原來陣列的元素在新陣列元素中的位置
void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
	//遍歷table陣列
        for (Entry<K,V> e : table) {
	    //遍歷以陣列元素為頭結點的連結串列
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
		//從新計算e在新陣列的位置
                int i = indexFor(e.hash, newCapacity);
		//e的next指向原先newTable[i]
                e.next = newTable[i];
		//將e放入陣列先的位置
                newTable[i] = e;
                e = next;
            }
        }
    }

        總結一下HashMap的存值的過程

        1.首先定位key對應陣列中的某一個位置

        2.在遍歷一下以這個位置的元素為表頭的連結串列

        3.檢視這個連結串列中是否有同樣的key值

                3.1 如果有,就用的新的value替換舊的value,到此就結束了

                3.2如果沒有,就將新的key-value放入到陣列中

        4.如果要放到陣列中,首先會判斷HashMap儲存的值得個數是否大於等於邊界值

               4.1 如果大於邊界值,會對陣列進行2倍擴容,擴容後會重新計算以前HashMap在新的陣列中的位置

        5.將新加入的Entry放入到根據對陣列相應的位置上,再讓Entry的next屬性指向原來的陣列元素

四.總結

        HashMap的資料結構就是由一個數組和多個連結串列組成的,陣列和連結串列中儲存的元素是Entry物件,Entry中有key,value,next,hashCode這幾個屬性,我們向HashMap中存放key-valu的其實是存入到了Entry物件中了。

         HashMap是對key的hashcode進行hash計算得到一個hash值,再用這個hash值與陣列長度減一進行於運算,得出key存在陣列中的某一個位置,如果陣列的這個位置已經有值了,這就產生了所謂的hash衝突,HashMap會將新加入的Entry放在陣列中,並讓Entry的next指向以前的陣列元素,這樣就在這裡產生了連結串列。

        在新加入元素的時候,當HashMap儲存值的個數即size大於或等於邊界值的時候,就會對陣列進行2倍擴容,這裡就是HashMap比較消耗新能的地方了,因為擴容後不僅要遍歷整個HashMap,而且還要重新計算每個元素在新的陣列中的位置。所以我們在初始化HashMap的時候可以指定陣列的大小,儘量減少陣列擴容。