本文參考資料: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。