《Java集合詳解系列》是我在完成夯實Java基礎篇的系列博客後準備開始寫的新系列。html
這些文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看java
喜歡的話麻煩點下Star、fork哈git
文章首發於個人我的博客:程序員
www.how2playlife.comgithub
今天咱們來探索一下HashSet,TreeSet與LinkedHashSet的基本原理與源碼實現,因爲這三個set都是基於以前文章的三個map進行實現的,因此推薦你們先看一下前面有關map的文章,結合使用味道更佳。面試
本文參考 http://cmsblogs.com/?p=599後端
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable
HashSet繼承AbstractSet類,實現Set、Cloneable、Serializable接口。其中AbstractSet提供 Set 接口的骨幹實現,從而最大限度地減小了實現此接口所需的工做。 ==Set接口是一種不包括重複元素的Collection,它維持它本身的內部排序,因此隨機訪問沒有任何意義。==微信
本文基於1.8jdk進行源碼分析。網絡
基本屬性
基於HashMap實現,底層使用HashMap保存全部元素
private transient HashMap<E,Object> map; //定義一個Object對象做爲HashMap的value private static final Object PRESENT = new Object();
構造函數
/** * 默認構造函數 * 初始化一個空的HashMap,並使用默認初始容量爲16和加載因子0.75。 */ public HashSet() { map = new HashMap<>(); } /** * 構造一個包含指定 collection 中的元素的新 set。 */ public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } /** * 構造一個新的空 set,其底層 HashMap 實例具備指定的初始容量和指定的加載因子 */ public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } /** * 構造一個新的空 set,其底層 HashMap 實例具備指定的初始容量和默認的加載因子(0.75)。 */ public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); } /** * 在API中我沒有看到這個構造函數,今天看源碼才發現(原來訪問權限爲包權限,不對外公開的) * 以指定的initialCapacity和loadFactor構造一個新的空連接哈希集合。 * dummy 爲標識 該構造函數主要做用是對LinkedHashSet起到一個支持做用 */ HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); } 從構造函數中能夠看出HashSet全部的構造都是構造出一個新的HashMap,其中最後一個構造函數,爲包訪問權限是不對外公開,僅僅只在使用LinkedHashSet時纔會發生做用。
既然HashSet是基於HashMap,那麼對於HashSet而言,其方法的實現過程是很是簡單的。
public Iterator<E> iterator() { return map.keySet().iterator(); }
iterator()方法返回對此 set 中元素進行迭代的迭代器。返回元素的順序並非特定的。
底層調用HashMap的keySet返回全部的key,這點反應了HashSet中的全部元素都是保存在HashMap的key中,value則是使用的PRESENT對象,該對象爲static final。
public int size() { return map.size(); } size()返回此 set 中的元素的數量(set 的容量)。底層調用HashMap的size方法,返回HashMap容器的大小。
public boolean isEmpty() { return map.isEmpty(); } isEmpty(),判斷HashSet()集合是否爲空,爲空返回 true,不然返回false。 public boolean contains(Object o) { return map.containsKey(o); } public boolean containsKey(Object key) { return getNode(hash(key), key) != null; } //最終調用該方法進行節點查找 final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //先檢查桶的頭結點是否存在 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //不是頭結點,則遍歷鏈表,若是是樹節點則使用樹節點的方法遍歷,直到找到,或者爲null if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
contains(),判斷某個元素是否存在於HashSet()中,存在返回true,不然返回false。更加確切的講應該是要知足這種關係才能返回true:(o==null ? e==null : o.equals(e))。底層調用containsKey判斷HashMap的key值是否爲空。
public boolean add(E e) { return map.put(e, PRESENT)==null; } public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } map的put方法: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //確認初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //若是桶爲空,直接插入新元素,也就是entry if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //若是衝突,分爲三種狀況 //key相等時讓舊entry等於新entry便可 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //紅黑樹狀況 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //若是key不相等,則連成鏈表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
這裏注意一點,hashset只是不容許重複的元素加入,而不是不容許元素連成鏈表,由於只要key的equals方法判斷爲true時它們是相等的,此時會發生value的替換,由於全部entry的value同樣,因此和沒有插入時同樣的。
而當兩個hashcode相同但key不相等的entry插入時,仍然會連成一個鏈表,長度超過8時依然會和hashmap同樣擴展成紅黑樹,看完源碼以後筆者才明白本身以前理解錯了。因此看源碼仍是蠻有好處的。hashset基本上就是使用hashmap的方法再次實現了一遍而已,只不過value全都是同一個object,讓你覺得相同元素沒有插入,事實上只是value替換成和原來相同的值而已。
當add方法發生衝突時,若是key相同,則替換value,若是key不一樣,則連成鏈表。
add()若是此 set 中還沒有包含指定元素,則添加指定元素。若是此Set沒有包含知足(e==null ? e2==null : e.equals(e2)) 的e2時,則將e2添加到Set中,不然不添加且返回false。
因爲底層使用HashMap的put方法將key = e,value=PRESENT構建成key-value鍵值對,當此e存在於HashMap的key中,則value將會覆蓋原有value,可是key保持不變,因此若是將一個已經存在的e元素添加中HashSet中,新添加的元素是不會保存到HashMap中,因此這就知足了HashSet中元素不會重複的特性。
public boolean remove(Object o) { return map.remove(o)==PRESENT; }
remove若是指定元素存在於此 set 中,則將其移除。底層使用HashMap的remove方法刪除指定的Entry。
public void clear() { map.clear(); }
clear今後 set 中移除全部元素。底層調用HashMap的clear方法清除全部的Entry。
public Object clone() { try { HashSet<E> newSet = (HashSet<E>) super.clone(); newSet.map = (HashMap<E, Object>) map.clone(); return newSet; } catch (CloneNotSupportedException e) { throw new InternalError(); } }
clone返回此 HashSet 實例的淺表副本:並無複製這些元素自己。
後記:
因爲HashSet底層使用了HashMap實現,使其的實現過程變得很是簡單,若是你對HashMap比較瞭解,那麼HashSet簡直是小菜一碟。有兩個方法對HashMap和HashSet而言是很是重要的,下篇將詳細講解hashcode和equals。
與HashSet是基於HashMap實現同樣,TreeSet一樣是基於TreeMap實現的。在《Java提升篇(二七)-----TreeMap》中LZ詳細講解了TreeMap實現機制,若是客官詳情看了這篇博文或者多TreeMap有比較詳細的瞭解,那麼TreeSet的實現對您是喝口水那麼簡單。
咱們知道TreeMap是一個有序的二叉樹,那麼同理TreeSet一樣也是一個有序的,它的做用是提供有序的Set集合。經過源碼咱們知道TreeSet基礎AbstractSet,實現NavigableSet、Cloneable、Serializable接口。
其中AbstractSet提供 Set 接口的骨幹實現,從而最大限度地減小了實現此接口所需的工做。
NavigableSet是擴展的 SortedSet,具備了爲給定搜索目標報告最接近匹配項的導航方法,這就意味着它支持一系列的導航方法。好比查找與指定目標最匹配項。Cloneable支持克隆,Serializable支持序列化。
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable
同時在TreeSet中定義了以下幾個變量。
private transient NavigableMap<E,Object> m; //PRESENT會被當作Map的value與key構建成鍵值對 private static final Object PRESENT = new Object();
其構造方法:
//默認構造方法,根據其元素的天然順序進行排序 public TreeSet() { this(new TreeMap<E,Object>()); } //構造一個包含指定 collection 元素的新 TreeSet,它按照其元素的天然順序進行排序。 public TreeSet(Comparator<? super E> comparator) { this(new TreeMap<>(comparator)); } //構造一個新的空 TreeSet,它根據指定比較器進行排序。 public TreeSet(Collection<? extends E> c) { this(); addAll(c); } //構造一個與指定有序 set 具備相同映射關係和相同排序的新 TreeSet。 public TreeSet(SortedSet<E> s) { this(s.comparator()); addAll(s); } TreeSet(NavigableMap<E,Object> m) { this.m = m; }
一、add:將指定的元素添加到此 set(若是該元素還沒有存在於 set 中)。
public boolean add(E e) { return m.put(e, PRESENT)==null; } public V put(K key, V value) { Entry<K,V> t = root; if (t == null) { //空樹時,判斷節點是否爲空 compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; //非空樹,根據傳入比較器進行節點的插入位置查找 if (cpr != null) { do { parent = t; //節點比根節點小,則找左子樹,不然找右子樹 cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; //若是key的比較返回值相等,直接更新值(通常compareto相等時equals方法也相等) else return t.setValue(value); } while (t != null); } else { //若是沒有傳入比較器,則按照天然排序 if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } //查找的節點爲空,直接插入,默認爲紅節點 Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; //插入後進行紅黑樹調整 fixAfterInsertion(e); size++; modCount++; return null; }
二、get:獲取元素
public V get(Object key) { Entry<K,V> p = getEntry(key); return (p==null ? null : p.value); }
該方法與put的流程相似,只不過是把插入換成了查找
三、ceiling:返回此 set 中大於等於給定元素的最小元素;若是不存在這樣的元素,則返回 null。
public E ceiling(E e) { return m.ceilingKey(e); }
四、clear:移除此 set 中的全部元素。
public void clear() { m.clear(); }
五、clone:返回 TreeSet 實例的淺表副本。屬於淺拷貝。
public Object clone() { TreeSet<E> clone = null; try { clone = (TreeSet<E>) super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(); } clone.m = new TreeMap<>(m); return clone; }
六、comparator:返回對此 set 中的元素進行排序的比較器;若是此 set 使用其元素的天然順序,則返回 null。
public Comparator<? super E> comparator() { return m.comparator(); }
七、contains:若是此 set 包含指定的元素,則返回 true。
public boolean contains(Object o) { return m.containsKey(o); }
八、descendingIterator:返回在此 set 元素上按降序進行迭代的迭代器。
public Iterator<E> descendingIterator() { return m.descendingKeySet().iterator(); }
九、descendingSet:返回此 set 中所包含元素的逆序視圖。
public NavigableSet<E> descendingSet() { return new TreeSet<>(m.descendingMap()); }
十、first:返回此 set 中當前第一個(最低)元素。
public E first() { return m.firstKey(); }
十一、floor:返回此 set 中小於等於給定元素的最大元素;若是不存在這樣的元素,則返回 null。
public E floor(E e) { return m.floorKey(e); }
十二、headSet:返回此 set 的部分視圖,其元素嚴格小於 toElement。
public SortedSet<E> headSet(E toElement) { return headSet(toElement, false); }
1三、higher:返回此 set 中嚴格大於給定元素的最小元素;若是不存在這樣的元素,則返回 null。
public E higher(E e) { return m.higherKey(e); }
1四、isEmpty:若是此 set 不包含任何元素,則返回 true。
public boolean isEmpty() { return m.isEmpty(); }
1五、iterator:返回在此 set 中的元素上按升序進行迭代的迭代器。
public Iterator<E> iterator() { return m.navigableKeySet().iterator(); }
1六、last:返回此 set 中當前最後一個(最高)元素。
public E last() { return m.lastKey(); }
1七、lower:返回此 set 中嚴格小於給定元素的最大元素;若是不存在這樣的元素,則返回 null。
public E lower(E e) { return m.lowerKey(e); }
1八、pollFirst:獲取並移除第一個(最低)元素;若是此 set 爲空,則返回 null。
public E pollFirst() { Map.Entry<E,?> e = m.pollFirstEntry(); return (e == null) ? null : e.getKey(); }
1九、pollLast:獲取並移除最後一個(最高)元素;若是此 set 爲空,則返回 null。
public E pollLast() { Map.Entry<E,?> e = m.pollLastEntry(); return (e == null) ? null : e.getKey(); }
20、remove:將指定的元素從 set 中移除(若是該元素存在於此 set 中)。
public boolean remove(Object o) { return m.remove(o)==PRESENT; }
該方法與put相似,只不過把插入換成了刪除,而且要進行刪除後調整
2一、size:返回 set 中的元素數(set 的容量)。
public int size() { return m.size(); }
2二、subSet:返回此 set 的部分視圖
/** * 返回此 set 的部分視圖,其元素範圍從 fromElement 到 toElement。 */ public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) { return new TreeSet<>(m.subMap(fromElement, fromInclusive, toElement, toInclusive)); } /** * 返回此 set 的部分視圖,其元素從 fromElement(包括)到 toElement(不包括)。 */ public SortedSet<E> subSet(E fromElement, E toElement) { return subSet(fromElement, true, toElement, false); }
2三、tailSet:返回此 set 的部分視圖
/** * 返回此 set 的部分視圖,其元素大於(或等於,若是 inclusive 爲 true)fromElement。 */ public NavigableSet<E> tailSet(E fromElement, boolean inclusive) { return new TreeSet<>(m.tailMap(fromElement, inclusive)); } /** * 返回此 set 的部分視圖,其元素大於等於 fromElement。 */ public SortedSet<E> tailSet(E fromElement) { return tailSet(fromElement, true); }
因爲TreeSet是基於TreeMap實現的,因此若是咱們對treeMap有了必定的瞭解,對TreeSet那是小菜一碟,咱們從TreeSet中的源碼能夠看出,其實現過程很是簡單,幾乎全部的方法實現所有都是基於TreeMap的。
LinkedHashSet是HashSet的一個「擴展版本」,HashSet並無論什麼順序,不一樣的是LinkedHashSet會維護「插入順序」。HashSet內部使用HashMap對象來存儲它的元素,而LinkedHashSet內部使用LinkedHashMap對象來存儲和處理它的元素。這篇文章,咱們將會看到LinkedHashSet內部是如何運做的及如何維護插入順序的。
咱們首先着眼LinkedHashSet的構造函數。在LinkedHashSet類中一共有4個構造函數。這些構造函數都只是簡單地調用父類構造函數(如HashSet類的構造函數)。 下面看看LinkedHashSet的構造函數是如何定義的。
//Constructor - 1 public LinkedHashSet(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor, true); //Calling super class constructor } //Constructor - 2 public LinkedHashSet(int initialCapacity) { super(initialCapacity, .75f, true); //Calling super class constructor } //Constructor - 3 public LinkedHashSet() { super(16, .75f, true); //Calling super class constructor } //Constructor - 4 public LinkedHashSet(Collection<? extends E> c) { super(Math.max(2*c.size(), 11), .75f, true); //Calling super class constructor addAll(c); }
在上面的代碼片斷中,你可能注意到4個構造函數調用的是同一個父類的構造函數。這個構造函數(父類的,譯者注)是一個包內私有構造函數(見下面的代碼,HashSet的構造函數沒有使用public公開,譯者注),它只能被LinkedHashSet使用。
這個構造函數須要初始容量,負載因子和一個boolean類型的啞值(沒有什麼用處的參數,做爲標記,譯者注)等參數。這個啞參數只是用來區別這個構造函數與HashSet的其餘擁有初始容量和負載因子參數的構造函數,下面是這個構造函數的定義,
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
顯然,這個構造函數內部初始化了一個LinkedHashMap對象,這個對象剛好被LinkedHashSet用來存儲它的元素。
LinkedHashSet並無本身的方法,全部的方法都繼承自它的父類HashSet,所以,對LinkedHashSet的全部操做方式就好像對HashSet操做同樣。
惟一的不一樣是內部使用不一樣的對象去存儲元素。在HashSet中,插入的元素是被當作HashMap的鍵來保存的,而在LinkedHashSet中被看做是LinkedHashMap的鍵。
這些鍵對應的值都是常量PRESENT(PRESENT是HashSet的靜態成員變量,譯者注)。
LinkedHashSet使用LinkedHashMap對象來存儲它的元素,插入到LinkedHashSet中的元素其實是被看成LinkedHashMap的鍵保存起來的。
LinkedHashMap的每個鍵值對都是經過內部的靜態類Entry<K, V>實例化的。這個 Entry<K, V>類繼承了HashMap.Entry類。
這個靜態類增長了兩個成員變量,before和after來維護LinkedHasMap元素的插入順序。這兩個成員變量分別指向前一個和後一個元素,這讓LinkedHashMap也有相似雙向鏈表的表現。
private static class Entry<K,V> extends HashMap.Entry<K,V> { // These fields comprise the doubly linked list used for iteration. Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } }
從上面代碼看到的LinkedHashMap內部類的前面兩個成員變量——before和after負責維護LinkedHashSet的插入順序。LinkedHashMap定義的成員變量header保存的是 這個雙向鏈表的頭節點。header的定義就像下面這樣,
接下來看一個例子就知道LinkedHashSet內部是如何工做的了。
public class LinkedHashSetExample { public static void main(String[] args) { //Creating LinkedHashSet LinkedHashSet<String> set = new LinkedHashSet<String>(); //Adding elements to LinkedHashSet set.add("BLUE"); set.add("RED"); set.add("GREEN"); set.add("BLACK"); } }
若是你知道LinkedHashMap內部是如何工做的,就很是容易明白LinkedHashSet內部是如何工做的。看一遍LinkedHashSet和LinkedHashMap的源碼, 你就可以準確地理解在Java中LinkedHashSet內部是如何工做的。
https://www.cnblogs.com/one-apple-pie/p/11036309.html
https://blog.csdn.net/learningcoding/article/details/79983248
若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!
Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。
黃小斜是跨考軟件工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長爲阿里工程師。
做者專一於 JAVA 後端技術棧,熱衷於分享程序員乾貨、學習經驗、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。
黃小斜是一個斜槓青年,堅持學習和寫做,相信終身學習的力量,但願和更多的程序員交朋友,一塊兒進步和成長!關注公衆號【黃小斜】後回覆【原創電子書】便可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》
程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。
本文由博客一文多發平臺 OpenWrite 發佈!