JDK學習---深刻理解java中的HashMap、HashSet底層實現

本文參考資料:html

一、《大話數據結構》java

二、http://www.cnblogs.com/dassmeta/p/5338955.html面試

三、http://www.cnblogs.com/dsj2016/p/5551059.html算法

四、http://blog.csdn.net/hackbuteer1/article/details/6591486/api

五、http://blog.csdn.net/feixiaoxing/article/details/6848077數組

六、http://www.cppblog.com/cxiaojia/archive/2012/07/31/185760.html數據結構

七、http://www.cnblogs.com/dolphin0520/p/3681042.htmlapp

 

  剛剛添加好《JDK學習---深刻理解java中的String》一篇的第四節數據結構部分,相信你們對線性表的順序存儲結構有必定的瞭解了吧。由於HashMap的底層就涉及到了鏈表,那麼接下來我就再介紹一下鏈表、尤爲是單鏈表的知識。post

 1、鏈表性能

  線性表的順序存儲結構,在上一篇博客《JDK學習---深刻理解java中的String》的第四節買火車的例子中已經說明了,它的最大缺點就是插入或刪除的時候,須要大量的移動元素,這顯然是耗時間的,在數據結構中可以有更加優化的方案呢?

  要解決這個問題,咱們就得思考一下致使這個問題的緣由。

  爲何插入和刪除時,須要大量移動元素,仔細分析後發現緣由在於相鄰的元素在存儲位置也具備鄰居關係,它們的編號分別爲1,2,3......,n。它們在內存中也是緊挨着的,中間沒有間隙,固然就沒法快速的介入,而刪除後,當中就會留下空隙,天然須要時間去彌補,這就是問題所在。

  接下來會引入鏈表,鏈表就是鏈式存儲的線性表。根據指針域的不一樣,鏈表分爲單向鏈表雙向鏈表循環鏈表等等。

  本文只介紹鏈表中最簡單的一種:單向鏈表。每一個元素包含兩個域,值域和指針域,咱們把這樣的元素稱之爲節點。每一個節點的指針域內有一個指針,指向下一個節點,而最後一個節點則指向一個空值。具體請看下圖: 

  

 

       從上圖咱們能夠看出來,每個節點,都會存在一個指針域,而這個指針域持有的是下一個節點的地址,這樣的話就能夠避免每一個節點元素在內存中存在鄰居關係,也就是說各個節點能夠分散存儲,每一個節點只須要持有下一個節點的內存地址便可。這樣也就避免了像線性表的順序存儲結構的缺點。

  單鏈表的插入:

     假設存儲元素e的節點爲s,那實現節點p、p->next和s之間的邏輯關係的變化,只須要將節點s插入到節點p和節點p->next之間便可。以下圖所示,單鏈表的插入根本不須要驚動其餘節點,只須要讓s->next和p->next 指針作一點改變便可。

 

  

s->next = p-> next;
p->next = s;

   解讀這兩句話,就是讓p的後繼節點改爲s的後繼節點,再把節點s變成p的後繼節點,以下圖:

單鏈表第i個數據插入節點的算法思路:

    一、聲明一個節點p指向鏈表的第一個節點,初始化j從1開始;

    二、當 j<i 時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一個節點,j累加1;

    三、若到鏈表末尾p爲空,則說明第i個元素不存在;

    四、不然查找成功,在系統中生成一個空節點s;

    五、將數據元素e 賦值給 s->next;

    六、單鏈表插入的標準語句:s->next = p-> next; p->next = s;

    七、返回成功。

思考:上面兩句節點操做語句是否能夠交換順序?

  若是先 p->next = s; 再 s->next = - ->next;會怎麼樣?由於第一句會使得將p->next給覆蓋成s的地址了。那麼s->next = p->next,其實就等於s->next = s;這樣真正的擁有的a<i+1> 數據元素的結點就沒有了上級。這樣的插入操做就是失敗的。須要注意

  

 單鏈表的刪除:

  

 

刪除的過程當中,咱們要作的,其實就是一步,p->next = p->next->next, 用q來取代 p->next, 即:

q=p->next; p->next = q->next;

   解讀這段代碼,也就是說讓p的後繼的後繼結點改爲p的後繼結點。

   舉個例子,爸爸的左手牽着媽媽的手,右手牽着寶寶的手在散步。忽然迎面來了一個美女,爸爸一會兒看呆了,此情此景被媽媽逮了個正着,因而她甩開牽着爸爸的手,繞過他,扯開父子倆,拉起寶寶的手就超前走去。媽媽是P節點,媽媽的後繼節點是爸爸p->next,也能夠叫作q節點。媽媽的後繼的後繼節點是兒子:p->next ->next,即q->next;當媽媽去牽兒子的手時,這個爸爸就已經與母子兩沒有任何關係了。以下圖:

 

單鏈表第i個數據刪除的算法思路:

    一、聲明一個節點p指向鏈表的第一個節點,初始化j從1開始;

    二、當 j<i 時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一個節點,j累加1;

    三、若到鏈表末尾p爲空,則說明第i個元素不存在;

    四、不然查找成功,將欲刪除的節點 p->next 賦值給q;

    五、單鏈表的刪除標準語句爲 p->next = q->next;

    六、將q節點中的數據賦值給e,做爲返回;

    七、釋放q節點,返回成功;

  

 單鏈表的讀取:

    線性表順序存儲結構中,咱們要查詢任意的存儲位置都很容易。但在單鏈表中,因爲第i個元素到底在哪?沒有辦法一開始就知道,必須從頭開始找。說白了就是從頭開始找,直到找到第i個元素爲止。因爲這個算法的時間複雜度取決於i的位置,當 i = 1時,則不須要遍歷,第一個就取出數據了;而當 i =n 時則遍歷n-1次才能夠。這是時間複雜度。

    因爲單鏈表的結構沒有定義表長,因此事先不能直到要循環多少次,所以也就不方便for來循環控制,其核心思想就是 「工做指針後移」,很麻煩。若是僅僅只是讀取,鏈表還不如線性表的順序存儲結構效率高呢!

 

2、HashSet底層解讀:

  JDK的API上說此類實現 Set 接口,由哈希表(其實是一個 HashMap 實例)支持。它不保證 set 的迭代順序;特別是它不保證該順序恆久不變。此類容許使用 null 元素。 

  爲何呢?下面看看源碼去一探究竟吧。

HashSet的成員變量:

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

add方法添加元素:直接將元素存儲到map中。

  public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

remove方法:直接將map中對應的元素移除。

  public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

isEmpty方法:

 public boolean isEmpty() {
        return map.isEmpty();
    }

remove方法:

   public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

iterator方法:

 public Iterator<E> iterator() {
        return map.keySet().iterator();
    }

 如今回憶一下常見的面試題

    一、HashSet集合是否有重複元素? (不能夠,由於HashMap的key不能夠重複)

  二、HashSet集合是否能夠有null?(能夠,由於HashMap的key能夠有一個null)

  三、HashSet元素是否有序?  (無序,由於HashMap的key無序)

  以上這些就是咱們經常使用的HashSet方法了吧,有沒有以爲好簡單,讀到這些代碼,有沒有以爲信心爆棚,此刻是否是膨脹了? jdk源碼原來so easy,哈哈!

 

3、HashMap底層實現

  以前是逗你玩呢,還真覺得JDK有這麼簡單啊,要是都這樣的級別,那java豈不是人人均可以成爲大神了?收拾收拾心情,回來繼續學習,如今進入JDK的入門代碼繼續研究吧!

認識一下HashMap結構圖吧:

 

再看HashMap的內部類Entry<K,V>,這個類是全文的中心點

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        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 Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        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) {
        }
    }

     這個類是幹什麼的呢?簡單點說,HashMap裏面保存的數據最底層是一個Entry型的數組,這個Entry則保留了一個鍵值對,還有一個指向下一個Entry的指針。因此HashMap是一種結合了數組和鏈表的結構。

 

你們調測代碼的時候,是否是根據一個功能逐步的去跟蹤代碼呢?如今咱們也按照這個思路去逐步分解HashMap方法吧?

 先看看put方法:

 

public V put(K key, V value) {
        //當key爲null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap容許爲null的緣由
        if (key == null)
            return putForNullKey(value);
        //計算key的hash值
        int hash = hash(key.hashCode());                 
        //計算key hash 值在 table 數組中的位置
        int i = indexFor(hash, table.length);            
        //從i出開始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //一、判斷該條鏈上是否有相同的hash值而且key相同,那麼此處直接找到table數組修改對應的e原始value值
            //二、若是此處hash值不一樣,則直接添加數組tablle
            //三、若是此處hash值相同,可是key不一樣。那麼找到數組下標就有可能重複,那麼此時table數組的同一個下標處就會存儲多個元素,它們是以鏈表形式存儲
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //舊值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回舊值
            }
        }
        //修改次數增長1
        modCount++;
        //將key、value添加至i位置處
        addEntry(hash, key, value, i);
        return null;
    }

 

  

 

 代碼分解:int hash = hash(key);這個方法是根據key生成一個hash值。兩個對象的存儲地址不一樣也有可能獲得相同的hashcode值,雖然這種機率極小,但仍是有這樣的概率存在的;所以,hash值也有可能重複的

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        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);
        

 代碼分解:int i = indexFor(hash, table.length);這個就是在table數組中返回一個下標索引,table是一個Entry<K,V>[]數組

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

進入主程序:

  一、咱們在使用put添加元素的時候,HashMap一開始是沒有key的,所以也不存在key,table數組也不可能存在數據。put的方法會進入到addEntry(hash, key, value, i)方法中

  二、addEntry一開始,也只是進入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);
    }

這個方法中有兩點須要注意:

      一是鏈的產生這是一個很是優雅的設計。系統老是將新的Entry對象添加到bucketIndex處。若是bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,造成一條Entry鏈,可是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。

      2、擴容問題

      隨着HashMap中元素的數量愈來愈多,發生碰撞的機率就愈來愈大,所產生的鏈表長度就會愈來愈長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必需要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。可是擴容是一個很是耗時的過程,由於它須要從新計算這些數據在新table數組中的位置並進行復制處理。因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。

 

三、進入createEntry方法: 

   void createEntry(int hash, K key, V value, int bucketIndex) {
     // 獲取指定 bucketIndex 索引處的 Entry Entry<K,V> e = table[bucketIndex];
      // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry   table[bucketIndex] = new Entry<>(hash, key, value, e);
     //記錄HashMap的長度 size++; }

  其實,咱們根據這一條線能夠發現,內部類Entry的構造方法,最後一個參數e,竟然對應的是next,這不就是鏈表的後繼節點嗎?

 Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

 

那麼,第一次put的過程,我是否是能夠這樣理解:

   a>、首先判斷key是否爲null,若爲null,則直接調用putForNullKey方法

   b>、而後根據hash值搜索在table數組中的索引位置,若是table數組在該位置處有元素,則經過比較是否存在相同的key,若存在則覆蓋原來key的value,不然將該元素保存在鏈頭(最早保存的元素放在鏈尾)。若table在該處沒有元素,則直接保存。

 

 那麼,若是同一個key、value進行第二次put怎麼辦呢?

仔細看看put方法,咱們看到了下面這段代碼,包含在put方法中的if語句塊:

 //從i出開始迭代 e,找到 key 保存的位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
        
       //判斷該條鏈上是否有hash值相同的(key相同)
           //若存在相同,則直接覆蓋value,返回舊value,這裏並無處理key,這就解釋了HashMap中沒有兩個相同的key
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }  
        }

  

get方法解讀:

先認識一下get(key)方法的源碼:

public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);
     
     //在本文的一開始,我就貼出了Entry<K,V>源碼,getValue()方法就是返回map中的value,能夠本身去本文開始的地方看
        return null == entry ? null : entry.getValue();
    }

裏面涉及到了getEntry(key)方法:這個方法,其實就是根據key生成的下標,到table數組中獲取Entry<K,V> e值並返回

 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;
    }

  

 size()方法:

這個size在createEntry方法方法中已經說明過了

 public int size() {
        return size;
    }

  

entryKey()、keySet()、values()方法解讀:

      篇幅有限,其餘方法留給本身去解讀吧!

   這邊我就重點說一下entryKey這個方法,keySet和values方法的原理與它都是同樣的。

   HashMap裏面保存的數據最底層是一個Entry型的數組,這個Entry則保留了一個鍵值對,還有一個指向下一個Entry的指針。因此HashMap是一種結合了數組和鏈表的結構。經過JDK的api文檔咱們也知道,entryKey()方法返回的是Set<Map.Entry<K,V>>的類型,所以咱們能夠經過迭代器遍歷出key/value鍵值對了,那麼底層究竟值怎麼作的呢?

  源碼解讀:

//一級方法,未作任何邏輯判斷,直接對entrySet0方法進行調用
  public Set<Map.Entry<K,V>> entrySet() {
        return entrySet0();
    }
  
  //此方法知識返回一個Set<Map.Entry<K,V>>類型的es,。從方法中咱們知道,entrySet有多是一個默認值,也有多是經過(entrySet = new EntrySet())方法生成的
	private Set<Map.Entry<K,V>> entrySet0() {
 
		//直接點擊entry方法進去查看,發現是抽象類HashIterator<E>中的private transient Set<Map.Entry<K,V>> entrySet = null;其中並未中任何的初始化
        Set<Map.Entry<K,V>> es = entrySet;
     
		//所以,我判斷第一次調用entrySet()方法的時候,是經過new方法生成的,下面去查看EntrySet類的源碼
        return es != null ? es : (entrySet = new EntrySet());
    }

  //EntrySet類型只有一個默認的構造方法,而且繼承了AbstractSet<Map.Entry<K,V>>抽象類,跟進去之後發現:
  //AbstractSet<E> extends AbstractCollection<E> implements Set<E>是一個繼承了Set接口,那麼最終EntrySet天然也就是一個Set類型的實現類了,而且它從新了Set接口的幾個方法,
   //源碼以下,這樣的話咱們調用entrySet()方法的時候,其實就是返回了實現Set接口的EntrySet的一個實例,而且這個實例從新了Set接口的方法,尤爲是iterator()方法

    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return newEntryIterator();
        }
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<K,V> e = (Map.Entry<K,V>) o;
            Entry<K,V> candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;
        }
        public int size() {
            return size;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

 咱們平時用entrySet()遍歷map,通常都是這樣作的:

      HashMap map = new HashMap();
	 	map.put("j1", "k1");
		map.put("j1", "k2");
		map.put("j3", "k3"); 
		
		Set<Map.Entry<String,String>> set = map.entrySet();
		for(Iterator iter = set.iterator(); iter.hasNext();)
		{
			Map.Entry<String,String> entry = (Entry<String, String>) iter.next();
			System.out.println("key :" + entry.getKey() + " , value : " + entry.getValue());
		} 

而此處使用set的迭代器方法iterator,此時的set是EntrySet的一個實例,所以此處的iterator()方法具體實現爲:

   private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return newEntryIterator();
        }
    ...........

 跟進去newEntryIterator()方法:

  Iterator<Map.Entry<K,V>> newEntryIterator()   {
        return new EntryIterator();
    }

 再次跟進EntryIterator類的實例:發現原來是重寫了next()方法

  private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
        public Map.Entry<K,V> next() {
            return nextEntry();
        }
    }

那麼咱們繼續跟進nextEntry()方法:

      final Entry<K,V> nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }

至此,咱們發現,原來咱們在本身的代碼中調用iterator()方法,最終在底層返回的是Entry<K,V> e類的實例。返回的接口並無實例化須要返回的參數,而是在調用返回的set實例的iterator()方法才初始化須要返回的Entry<K,V>類型,而我在本文一開始的地方,就將Entry<K,V>類源碼就貼出來了,細心的朋友能夠能已經發現,此類已經提供了直接返回key、value的方法了,所以咱們能夠直接調用。

 

 一個不得不說的方法,remove(Key)方法:

 public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

 這個方法沒幹什麼事情,只是簡單的調用了removeEntryForKey(Key)方法了,下面看看這個方法幹了什麼事情:

final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry<K,V> prev = table[i]; Entry<K,V> e = prev;

        while (e != null) {
            Entry<K,V> next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }
 

     這個方法我最想提起的是上面標紅的部分: Entry<K,V> prev = table[i];Entry<K,V> e = prev;  那下面再判斷 if (prev == e){     table[i] = next; }有意思嗎?請你們思考,這個地方判斷有意思嗎?有意義嗎?

       這個地方,一開始我也不是很明白,感受這不就是一個指針麼,實例e指向了prev了,那麼prev == e這個邏輯應該是恆成立的纔對呀?

       其實,由於HashMap的底層實現是鏈表,而鏈表的插入和刪除的實現思路,在上面的說鏈表的時候已經提起了,這裏須要指出的是Entry<K,V> prev = table[i];這實際上是table數組的一個元素而已,可是這個元素自己底層倒是一個Entry<Key,Value>類型的鏈表,也就是說可能有不少元素。那麼咱們在定義一個節點Entry<K,V> e = prev 指向perv,其實只是指向prev鏈表的第一個節點;若是prev == e,那就說明此處的鏈表僅有一個節點,並且這個節點沒有後繼節點。不然,prev確定不等於e。

相關文章
相關標籤/搜索