java之jdk 1.8版本之前的HashMap

1. HashMap的數據結構

數據結構中有數組、鏈表來實現對數據的存儲,接下來講解HashMap

數組

數組存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,爲O(1);數組的特點是:尋址容易,插入和刪除困難;
 

鏈表

鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。

哈希表

那麼我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表。哈希表((Hash table)既滿足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。

  哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法—— 拉鍊法,我們可以理解爲“鏈表的數組” ,如圖:



從上圖我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標爲12的位置,插入的順序下面有介紹頭插法
鏈表是有多個節點構成,每個節點稱爲Entry節點,包含四個屬性分別是:Key(鍵對象)、value(值對象)、hash(鍵對象的hash值)、next(指向鏈表中下一個Entry對象引用地址)

重點:1、查找數據是首先會根據過hash(key)%len獲得數據地址,在循環遍歷整個鏈表獲取數據

           2、有一個初始容量大小,默認是16,並且默認加載因子是0.75,當容量超過16x0.75=12時,就會觸發擴容操作,就把數組的大小擴展爲2*16=32,即擴大一倍,一旦觸發擴容,已經存在的數據會重新計算存儲位置,所以儘量避免擴容
           3、如果傳key傳空或者0,那麼他們的hash值是0
           4、鏈表插入方式是使用頭插法,最後插入的數據是head,最後一位NUll就是鏈表結束

 

問題1:什麼是hash衝突,hash如何解決hash衝突問題?
答:判斷當前確定的索引位置是否存在相同hashcode和相同key的元素,如果存在相同的hashcode和相同的key的元素,那麼新值覆蓋原來的舊值,並返回舊值。  
            //如果存在相同的hashcode,那麼他們確定的索引位置就相同,這時判斷他們的key是否相同,如果不相同,這時就是產生了hash衝突。  
解決方法:鏈表法(默認)和開放地址法
鏈表法:在鏈表中新增一個節點用來存放重複的hash,遍歷鏈表就可以獲取

問題2:hashMap死循環是什麼情況發生的
我們都知道HashMap初始容量大小爲16,一般來說,Hash表這個容器當有數據要插入時,都會檢查容量有沒有超過設定的thredhold,如果超過,需要增大Hash表的尺寸,但是這樣一來,整個Hash表裏的元素都需要被重算一遍。這叫rehash

HashMap是非線程安全,死循環一般都是產生於併發情況下。我們假設有二個進程T1、T2,HashMap容量爲2,T1線程放入key A、B、C、D、E。在T1線程中A、B、C Hash值相同,於是形成一個鏈接,假設爲A->C->B,而D、E Hash值不同,於是容量不足,需要新建一個更大尺寸的hash表,然後把數據從老的Hash表中 
遷移到新的Hash表中(refresh)。這時T2進程闖進來了,T1暫時掛起,T2進程也準備放入新的key,這時也 
發現容量不足,也refresh一把。refresh之後原來的鏈表結構假設爲C->A  (原因是擴容hash重新計算和排序,插入方式是頭插法),之後T1進程繼續執行,鏈接結構 
爲A->C,這時就形成A.next=B,B.next=A的環形鏈表。一旦取值進入這個環形鏈表就會陷入死循環。
https://blog.csdn.net/fhzaitian/article/details/51505516

2. HashMap的存取實現

既然是線性數組,爲什麼能隨機存取?這裏HashMap用了一個小算法,大致是這樣實現:
// 存儲時:
int hash = key.hashCode(); // 這個hashCode方法這裏不詳述,只要理解每個key的hash是一個固定的int值
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:
int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

(1)、put 方法
 public V put(K key, V value) {

        if (key == null)

            return putForNullKey(value); //null總是放在數組的第一個鏈表中

        int hash = hash(key.hashCode());

        int i = indexFor(hash, table.length);

        //遍歷鏈表

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {

            Object k;

            //如果key在鏈表中已存在,則替換爲新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++;

        addEntry(hash, key, value, i);

        return null;

    }

 

void addEntry(int hash, K key, V value, int bucketIndex) {

    Entry<K,V> e = table[bucketIndex];

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //參數e, 是Entry.next

    //如果size超過threshold,則擴充table大小。再散列

    if (size++ >= threshold)

            resize(2 * table.length);

}

(2)、get方法

 public V get(Object key) {

        if (key == null)

            return getForNullKey();

        int hash = hash(key.hashCode());

        //先定位到數組元素,再遍歷該元素處的鏈表

        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.equals(k)))

                return e.value;

        }

        return null;

}


(3)、key爲NULL的獲取

null key總是存放在Entry[]數組的第一個元素。

   private V putForNullKey(V value) {

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {

            if (e.key == null) {

                V oldValue = e.value;

                e.value = value;

                e.recordAccess(this);

                return oldValue;

            }

        }

        modCount++;

        addEntry(0, null, value, 0);

        return null;

    }

 

    private V getForNullKey() {

        for (Entry<K,V> e = table[0]; e != null; e = e.next) {

            if (e.key == null)

                return e.value;

        }

        return null;

    }

 

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