Java HashSet源码解析
本解析源碼來自JDK1.7,HashSet是基于HashMap實(shí)現(xiàn)的,方法實(shí)現(xiàn)大都直接調(diào)用HashMap的方法
另一篇HashMap的源碼解析文章
概要
- 實(shí)現(xiàn)了Set接口,實(shí)際是靠HashMap實(shí)現(xiàn)的
- 不保證遍歷時(shí)的順序,不保證集合順序的不變性
- HashSet允許出現(xiàn)null值
- 假定Hash算法能很好的分散元素,查詢的時(shí)間復(fù)雜度為O(1)
- 遍歷的時(shí)間復(fù)雜度由set的size和其依靠的HashMap的capacity來決定
- HashSet是非同步的可以通過Set s = Collections.synchronizedSet(new HashSet(...));的方式獲得同步的set
- HashSet的Iterator有fast-fail機(jī)制,但是并不能保證程序一定正確,fail-fast機(jī)制通常只用來檢測bug
實(shí)現(xiàn)接口
- Set接口包含集合常用方法
- Cloneable 對集合元素進(jìn)行淺拷貝,調(diào)用了map的clone方法來克隆自身map域,由于HashMap執(zhí)行的是淺拷貝,雖然創(chuàng)建了新的Entry,但是沒有創(chuàng)建新的key和value,通過原Set和clone后的set對key和value的改變是等效的。
- Serializable HashSet實(shí)現(xiàn)了自己readObject和writeObject方法,將map的keySet中的元素分別寫入,對端讀出后放入自己的map中去
主要成員
- map用來裝載set的元素,HashSet就是HashMap的keySet。transient表明序列化時(shí)略過,HashSet實(shí)現(xiàn)了自己的序列化方法。
- 常量PRESENT用來填充HashMap的value,也就是說HashSet中的map的value都是同一個對象,高效的利用堆空間
NOTE:所有被裝入集合(map,set,list)的對象,都只是將對象的引用復(fù)制一份到集合中。如果通過外部引用改變了對象的內(nèi)容,集合中的對象的內(nèi)容也會跟著改變。但是如果將外部引用指向其他對象,集合內(nèi)部的引用并不會改變,還是會指向加入集合時(shí)引用指向的對象。也即元素被放入集合時(shí),執(zhí)行的是淺拷貝,引用復(fù)制而對象不會重新創(chuàng)建
private transient HashMap<E,Object> map;// Dummy value to associate with an Object in the backing Mapprivate static final Object PRESENT = new Object();構(gòu)造函數(shù)
我們看到初始化HashSet就是初始化對應(yīng)的HashMap對象map成員變量
public HashSet() {map = new HashMap<>();} public HashSet(Collection<? extends E> c) {map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));addAll(c);}public HashSet(int initialCapacity, float loadFactor) {map = new HashMap<>(initialCapacity, loadFactor);}public HashSet(int initialCapacity) {map = new HashMap<>(initialCapacity);}集合相關(guān)方法
可以看到對HashSet的操作就是對對應(yīng)的HashMap對象map的操作
public Iterator<E> iterator() {return map.keySet().iterator();}public int size() {return map.size();}public boolean isEmpty() {return map.isEmpty();}public boolean contains(Object o) {return map.containsKey(o);}public boolean add(E e) {return map.put(e, PRESENT) == null;}public boolean remove(Object o) {return map.remove(o) == PRESENT;}public void clear() {map.clear();}HashMap解決擴(kuò)容問題
- 調(diào)整的時(shí)機(jī) (負(fù)載因子)x(容量)>(Map 大小),則調(diào)整 Map大小 為之前的二倍,該過程包含了table的復(fù)制,性能消耗較大,如果map大小已知,可以在初始化時(shí)合理設(shè)定map的初始大小,避免擴(kuò)容。
- 如果數(shù)組大小已經(jīng)到達(dá)最大容量,將閾值置為Integer.MAX_VAlUE,不再進(jìn)行擴(kuò)容
- 新申請數(shù)組,重新定址并將原數(shù)組中的Entry轉(zhuǎn)移到新數(shù)組中,由于容量變化,即使Hash值不變,Entry的index也會改變,index=hash&(length-1),取hash的低位,length增大,index取的位數(shù)增多
- 依舊使用頭插法將所有元素進(jìn)行復(fù)制
HashMap 元素存儲位置的計(jì)算 hash值
- String類型的key的hashcode是根據(jù)與字符串內(nèi)容相關(guān)的,由于可能引起很多碰撞,所以值單獨(dú)計(jì)算
- Object類型的key的HashCode是基于其內(nèi)存地址的。為了充分利用Integer值的高位,需要將高位的影響引入低位,(由于多數(shù)map的length是比較小的)
- 由于length是2的指數(shù)倍,所以可以用hash&(length-1)代替 hash%length,位運(yùn)算有更高的效率
HashMap的put方法
下面以HashMap的put方法為例對HashSet的集合方法進(jìn)行說明
- 如果是key為null,遍歷查找table中key是否有null,如果有更新value,否則添加null,value節(jié)點(diǎn)
- 如果key不為null,根據(jù)Key的hashcode獲取Hash值,根據(jù)Hash計(jì)算其在table中的索引。hash值計(jì)算時(shí)利用高位與低位進(jìn)行異或操作,加入高位因素,來減少Hash碰撞。
- 由于tablelength 都是2的指數(shù)次冪,所以indexFor用 HashCode&(table.lenght-1)取HashCode的低位,使用位運(yùn)算提高運(yùn)算效率
- 如果table[i]不為null(并不表示Hash值相同,HashCode不同也可能碰撞),也就是發(fā)生了Hash碰撞,如果存在與keyHash相等(equals)或相同(==)的key,那么更新value
- 如果table[i]為null,或者table[i]鏈表中不存在Hash值與Key相同且equals函數(shù)返回true的情況就根據(jù)Hash值添加新的節(jié)點(diǎn)
- addEntry()方法首先判斷大小是否超過閾值,然后使用頭插法,插入元素
NOTE
在判斷插入Entry是否為覆蓋時(shí),會先判斷Key的hashCode是否和map中的key相等,然后判斷Equals方法或者==,所以如果重寫了equals方法,要記得重寫hashcode方法,使得其邏輯相同,否則即使equals方法判斷相等也不會發(fā)生覆蓋
Clone方法
Clone方法是淺拷貝方法
Clone就是將對應(yīng)的map進(jìn)行復(fù)制
HashMap的Clone方法
Clone實(shí)現(xiàn)的是淺拷貝,雖然重新創(chuàng)建了Entry但是并沒有重新創(chuàng)建key,value,即如果通過原HashMap的key的引用改變了key的屬性,clone出來的HashMap的key也會跟著改變,克隆出來的Map的數(shù)組的大小也不一定與原Map相同
- HashSet的Clone方法其實(shí)就是對成員變量map的clone
- 首先會創(chuàng)建一個空的HashMap對象
- 然后對該HashMap進(jìn)行擴(kuò)容,容量大小取Math.min(當(dāng)前table大小,HashMap的最大容量,當(dāng)前的Size*(Math.min(1/loadFactor,4)),克隆出來的HashMap的數(shù)組初始大小并不會與當(dāng)前Map一致,而是考慮合理的初始化loadFactor之后的結(jié)果。
- 最后調(diào)用putAllForCreate(this)依次將當(dāng)前Map的(key,value)放到Map中去,過程中雖然創(chuàng)建了新的Entry但是并沒有創(chuàng)建新的key,value,通過原HashMap和通過克隆出來的HashMap改變(key,value)效果是等同的。
序列化方法
序列化方法就是將Map keySet中的元素依次寫出,然后在對端依次讀入,重建HashMap
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException {// Write out any hidden serialization magics.defaultWriteObject();// Write out HashMap capacity and load factors.writeInt(map.capacity());s.writeFloat(map.loadFactor());// Write out sizes.writeInt(map.size());// Write out all elements in the proper order.for (E e : map.keySet())s.writeObject(e);}private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {// Read in any hidden serialization magics.defaultReadObject();// Read in HashMap capacity and load factor and create backing HashMapint capacity = s.readInt();float loadFactor = s.readFloat();map = (((HashSet)this) instanceof LinkedHashSet ?new LinkedHashMap<E,Object>(capacity, loadFactor) :new HashMap<E,Object>(capacity, loadFactor));// Read in sizeint size = s.readInt();// Read in all elements in the proper order.for (int i=0; i<size; i++) {E e = (E) s.readObject();map.put(e, PRESENT);}}總結(jié)
以上是生活随笔為你收集整理的Java HashSet源码解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: wordpress 首页调用文章 不同样
- 下一篇: 大数据Java基础第十九天作业