map的key就是一個set,而且map提供了一個返回key集合:Set<k> ketset()java
而從set到map,set中的每一個元素都是k-v對便可算法
因爲給出key,map總能夠找到對應的value,因此能夠將value當作key的附屬物數組
HashSet與HashMap性能
兩個都是經過Hash算法來計算集合元素的存儲位置this
集合存儲Java對象並非指將java對象放在集合中,而是集中保留了這些對象的引用(指針),這些引用變量指向實際的java對象spa
能夠看出,list中存儲了兩個對象,這個兩個對象最終仍是指向了堆內存中的實際java對象,即說明list中存儲的是對象的引用debug
它是Map的一個內部接口,每一個Map.Entry實際就是一個k-v對,而且系統在存儲k-v對時,沒有考慮v,而是僅僅根據k來計算Entry的位置,位置肯定以後,value也隨之保存了指針
當調用put方法時,即試圖將一個k-v對放入一個HashMap時,首先根據key的hashCode()返回值決定Entry的存儲位置,若是兩個Entry的key的hashCode值相同,那麼他們對應的v就被新的v覆蓋,k不會覆蓋,若是不一樣,則新添加的Entry將於原有的Entry造成Entry鏈,而且新添加的Entry位於Entry鏈的頭部code
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); //計算出大於initCapacity的最小的2的n次方冪 /*當實際容量小於初始容量時,使用位移運算,將實際容量不斷的乘以2(最終的效果就是乘以2的n次方),直至實際容量大於初始容量*/ // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; //設置容量極限等於容量乘以負載因子 this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); //初始化table數組 table = new Entry[capacity]; init(); }
通過debug發現初始化數組的時候,居然也會調用上面這個HashMap的構造方法…對象
查看源碼發現
HashMap的底層經過數組來存儲數據的,該數組的長度不必定是指定的初始長度,而是比初始值大的最小的2的n次方,因此儘可能使初始值是2的n次方,這樣能夠減少系統開銷。
HashMap及其子類採用Hash算法來決定集合中元素的位置,系統初始化HashMap時,系統會建立一個長度爲capacity的數組Entry,該數組裏能夠存儲元素的位置被稱爲桶(bucket),每一個bucket都有其指定的索引,經過索引,系統能夠快速的訪問到bucket裏存儲的元素
每一個bucket只能存儲一個元素,即一個Entry,可是因爲Entry對象能夠包含一個引用變量用於指向下一個Entry,因此可能出現bucket中只有一個Entry,但這個Entry指向另外一個Entry,造成Entry鏈。
若是bucket只有一個Entry,系統先計算某個key的hashCode值,在根據hashCode值找到該key在table數組中的索引,而後取出該索引處的Entry,最後返回該key對應的value
若是bucket存儲的是一個Entry鏈,那麼找到該Entry以後還要遍歷該Entry鏈才能找到特定的Entry
源碼
public V get(Object key) { //若是key是null,調用getForNullKey取出value if (key == null) return getForNullKey(); //計算key的hashCode int hash = hash(key.hashCode()); //直接取出table數組中指定索引處的值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; //搜索該Entry鏈的下一個entry e = e.next) { Object k; //若是該Entry的key和被搜索的key相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; }
建立HashMap時,有一個默認的負載因子(load factor),默認爲0.75,增大負載因子能夠減小hash表所佔的空間,但會增長查詢時間,而最頻繁的操做put和get都須要用到查詢;反之,減少負載因子會提升查詢效率可是會下降存儲空間
查看HashSet源碼
它只是封裝了一個HashMap,它所存儲的元素其實是由HashMap的key來保存的,而該HashMap的value則是一個PRESENT,一個靜態的java對象
HashMap和HashSet判斷重複的標準包括兩個對象的hashCode值相等,而且二者equals~!若是要判斷本身的類對象是否重複,也須要重寫hashCode和equals~!
例如:
若是不重寫hashCode則不能正確判斷兩個對象是否相等
TreeSet底層實際上也是有TreeMap實現的
對於TreeMap,它採用紅黑樹的排序二叉樹來保存Map中每一個Entry—每一個Entry都被當作紅黑樹的一個節點來對待。
當向TreeMap中存入數據的時候,首先會把第一個元素的Entry做爲根節點,後面添加的元素的Entry都做爲新節點添加到已知的紅黑樹中,這樣就保證了TreeMap中的key按照由小到大的順序排列。(該二叉樹自己就有默認的排序)
TreeMap和HashMap相比,性能較低,由於在查詢和插入元素的時候須要不斷的循環,找到合適Entry(的位置),可是它優點是有序。
關於紅黑樹:
TreeMap的關鍵就是put(K key,V vlaue), 該方法實現將Entry放入到TreeMap的Entry鏈。
源碼:
分析源代碼:
首先該map若是爲空,即沒有任何元素,t==null,那麼就建立一個新的Entry,並將該Entry做爲root;
接着,就要去尋找合適的put的位置了,經過判斷用戶是否認制了Comparator,若是有定製,則使用定製的排序方式,不然就使用默認的(新建的)Comparaotor,該比較器的做用是找到合適的put的節點,並把該節點做爲即將被put的元素的父親節點;
找尋的方式是:首先將根節點做爲父親節點,而後經過Compaerator判斷該父親節點的key與被put的key的大小,若是父親節點大,那麼就將父親節點的左節點做爲新的父親節點;反之則將父親節點的右節點做爲新的父親節點;若是相等,則覆蓋原有節點上的value,並經過return返回,再也不循環;
一輪循環結束後再去判斷新的父親節點是否真實存在,若是存在,則繼續循環遍歷,不然退出循環。
最終的結果就是,找到了該TreeMap中合適位置的節點
最後將循環找到的節點做爲新添加節點的父節點,再次去判讀父節點的key和新添加節點的key的大小,父親節點大,則添加在父親節點的左節點,反之則在右節點(不存在相等了)。
使用get方法查找元素時:
最終調用的仍是getEntry()方法:
能夠看出,getEntry()方法也是首先判斷是否有定製的Comparator,而後經過Comparator去遍歷查詢出須要的節點,遍歷方式和put方法一致。
綜上:TreeMap本質上就是一顆紅黑樹,而每一個Entry就是樹的一個節點。