轉自:點擊打開鏈接
在Java容器學習筆記(一)中概述了Collection的基本概念及接口實現,並且總結了它的一個重要子接口List及其子類的實現和用法。
本篇主要總結Set接口及其實現類的用法,包括HashSet(無序不重複),LinkedHashSet(按放入順序有序不重複),TreeSet(按紅黑樹方式有序不重複),EnumSet,ConcurrentSkipListSet(來自於java.util.concurrent包),CopyOnWriteArraySet(來自於java.util.concurrent包)等。
2. Set接口及其實現類
Set接口中方法清單:
Set集合和List集合都存放的是單個元素的序列,但是Set集合不允許集合中有重複元素(主要依賴於equals方法)。
Set接口的父接口爲Collection和Iterable,直接實現該接口的子接口有SortedSet和NavigableSet。
實現Set接口的重要類有HashSet(無序不重複),LinkedHashSet(按放入順序有序不重複),TreeSet(按紅黑樹方式有序不重複),EnumSet,ConcurrentSkipListSet(來自於java.util.concurrent包),CopyOnWriteArraySet(來自於java.util.concurrent包)。
在Set接口中沒有新增任何方法,所有方法均來自其父接口。它無法提供像List中按位存取的方法。在數學上一個集合有三個性質:確定性,互異性,無序性。
Ø HashSet的特點、實現機制及使用方法
a) HashSet的特點:
HashSet中存放的元素是無序的,底層是用HashMap實現的,其中key是要放入的元素,value是一個Object類型的名爲PRESENT的常量,由於用到了散列函數,因此其存取速度是非常快的,在地址空間很大的情況下它的存取速度可以達到O(1)級。如果首先了解了HashMap的實現方法,那麼HashSet的實現是非常簡單的。
b)HashSet的實現機制:
首先需要了解一下散列或者哈希的用法。我們知道,當數據量很大時hash函數計算的結果將會重複,按照下圖所示的形式進行存貯。
在HashSet中有個loadFactor(負載因子),對於上圖所示總共有11個位置,目前有4個位置已經存放,即40%的空間已被使用。
在HashSet的默認實現中,初始容量爲16,負載因子爲0.75,也就是說當有75%的空間已被使用,將會進行一次再散列(再哈希),之前的散列表(數組)將被刪除,新增加的散列表是之前散列表長度的2倍,最大值爲Integer.MAX_VALUE。
負載因子越高,內存使用率越大,元素的尋找時間越長。
負載因子越低,內存使用率越小,元素的尋找時間越短。
從上圖可以看出,當哈希值相同時,將存放在同一個位置,使用鏈表方式依次鏈接下去。
(面試官問到這個問題,當時我的回答是再哈希,其實我並不知道HashSet真正是怎麼實現的,我只知道在學習數據結構時學習過再哈希,就是這個哈希表很滿時需要重新建立哈希表,以便於存取,因爲大量的值放在一個位置上就變成了鏈表的查詢了,幾乎是O(n/2)級別的,但是我沒有說出來再哈希的過程,以及哈希值相同時到底如何存放,所以……~~o(>_<)o ~~)。
爲了說明HashSet在Java中確實如上實現,下面附上JDK中兩個重要方法的源碼:(下面源碼來自於HashMap,原因是HashSet是基於HashMap實現的)
- /**
- * Rehashes the contents of this map into a new array with a
- * larger capacity. This method is called automatically when the
- * number of keys in this map reaches its threshold.
- *
- * If current capacity is MAXIMUM_CAPACITY, this method does not
- * resize the map, but sets threshold to Integer.MAX_VALUE.
- * This has the effect of preventing future calls.
- *
- * @param newCapacity the new capacity, MUST be a power of two;
- * must be greater than current capacity unless current
- * capacity is MAXIMUM_CAPACITY (in which case value
- * is irrelevant).
- */
- void resize(int newCapacity) {
- Entry[] oldTable = table;
- int oldCapacity = oldTable.length;
- if (oldCapacity == MAXIMUM_CAPACITY) {
- threshold = Integer.MAX_VALUE;
- return;
- }
- Entry[] newTable = new Entry[newCapacity];
- transfer(newTable);
- table = newTable;
- threshold = (int)(newCapacity * loadFactor);
- }
- /**
- * Transfers all entries from current table to newTable.
- */
- void transfer(Entry[] newTable) {
- Entry[] src = table;
- int newCapacity = newTable.length;
- for (int j = 0; j < src.length; j++) {
- Entry<K,V> e = src[j];
- if (e != null) {
- src[j] = null;
- do {
- Entry<K,V> next = e.next;
- int i = indexFor(e.hash, newCapacity);
- e.next = newTable[i];
- newTable[i] = e;
- e = next;
- } while (e != null);
- }
- }
- }
HashSet共實現了5個構造方法,對外提供了4個構造方法。這些方法在api中均可看到詳細使用說明。由於HashSet基於HashMap實現,我們只關心我們放入的key,value是個Object類型的常量,所以在iterator方法中使用的是HashMap的keySet方法進行迭代的。
c)HashSet的使用方法:
從HashSet的特點及實現上看,我們知道在不需要放入重複數據並且不關心放入順序以及元素是否要求有序的情況下,我們沒有任何理由不選擇使用HashSet。另外HashSet是允許放空值的。
那麼HashSet是如何保證不重複的?下面一個例子說明:
- import java.util.HashSet;
- import java.util.Iterator;
- public class ExampleForHashSet {
- public static void main(String[] args) {
- HashSet<Name> hs = new HashSet<Name>();
- hs.add(new Name("Wang", "wu"));
- hs.add(new Name("Zhang", "san"));
- hs.add(new Name("Wang", "san"));
- hs.add(new Name("Zhang", "wu"));
- //本句輸出爲2
- System.out.println(hs.size());
- Iterator<Name> it = hs.iterator();
- //下面輸出兩行,分別爲Zhang:san和Wang:wu
- while(it.hasNext()) {
- System.out.println(it.next());
- }
- }
- }
- class Name {
- String first;
- String last;
- public Name(String first, String last) {
- this.first = first;
- this.last = last;
- }
- @Override
- public boolean equals(Object o) {
- if(null == o) {
- return false;
- }
- if(this == o) {
- return true;
- }
- if(o instanceof Name) {
- Name name = (Name)o;
- //本例認爲只要first相同即相等
- if(this.first.equals(name.first)) {
- return true;
- }
- }
- return false;
- }
- @Override
- public int hashCode() {
- int prime = 31;
- int result = 1;
- //hashcode的實現一定要和equals方法的實現對應
- return prime*result + first.hashCode();
- }
- @Override
- public String toString() {
- return first + ":" + last;
- }
- }
簡單說明一下上面的例子:
上面已經提到HashSet裏面放的元素是不允許重複的,那麼什麼樣的元素是重複呢,重複的定義是什麼?
上面例子中實現了一個簡單的類Name類,並且重寫了equals方法與hashCode方法,那麼重複指的是equals方法嗎?equals相同就算是重複嗎?當然不是這樣的。如果我們改寫一下hashCode方法,將返回值改爲
return prime*result + first.hashCode() + last.hashCode()
那麼HashSet中的size會變爲4,但是Name(“Wang”, “wu”)和Name(“Wang”, “san”)其實用equals方法來比較的話其實是相同的。
Name n1 = new Name("W", "x");
Name n2 = new Name("W", "y");
System.out.println(n1.equals(n2));
也就是說上面代碼會輸出true。
這樣我們是不是可以這樣認爲:如果hashCode相同的話再判斷equals的返回值是否爲true,如果爲true則相同,即上面說的重複。如果hashCode不同那麼一定是不重複的?
由此看來equals相同,hashCode不一定相同,equals和hashCode的返回值不是絕對關聯的?當然我們實現equals方法時是要根據hashCode方法實現的,必須建立關聯關係,也就是說正常情況下equals相同,則hashCode的返回值應該是相同的。
Ø LinkedHashSet的特點、實現機制及使用方法
a) LinkedHashSet的特點:
LinkedHashSet保證了按照插入順序有序,繼承自HashSet,沒有實現新的可以使用的方法。
b) LinkedHashSet實現機制:
LinkedHashSet繼承自HashSet,構造時使用了在HashSet中被忽略的構造方法:
- /**
- * Constructs a new, empty linked hash set. (This package private
- * constructor is only used by LinkedHashSet.) The backing
- * HashMap instance is a LinkedHashMap with the specified initial
- * capacity and the specified load factor.
- *
- * @param initialCapacity the initial capacity of the hash map
- * @param loadFactor the load factor of the hash map
- * @param dummy ignored (distinguishes this
- * constructor from other int, float constructor.)
- * @throws IllegalArgumentException if the initial capacity is less
- * than zero, or if the load factor is nonpositive
- */
- HashSet(int initialCapacity, float loadFactor, boolean dummy) {
- map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
- }
由上面JDK代碼可以看出LinkedHashSet底層是使用LinkedHashMap實現的。
所以在實現上是比較簡單的,是根據dummy這個參數,我們不需要傳入,選擇構造的是HashSet還是LinkedHashSet。
c) LinkedHashSet的使用方法:
由於LinkedHashSet繼承自HashSet,並且沒有提供額外的供使用的方法,所以在使用時與HashSet基本相同,只是面臨的是選擇的問題。我們根據需要選擇不同的數據結構來實現我們的需求。
Ø CopyOnWriteArraySet的特點、實現機制及使用方法
a) CopyOnWriteArraySet的特點:
CopyOnWriteArraySet是java.util.concurrent包中的一個類,繼承自AbstractSet,底層使用CopyOnWriteArrayList實現,擁有Set的特點,也具有ArrayList的特點,並且是線程安全的類。
b) CopyOnWriteArraySet的實現機制:
在實現時使用了寫時拷貝的方法以及使用重入鎖實現了線程的同步,底層使用CopyOnWriteArrayList來構造出一個實例對象,在添加元素時調用CopyOnWriteArrayList的addIfAbsent方法保證數據不重複,其它實現與CopyOnWriteArrayList類似。
c) CopyOnWriteArraySet的使用方法:
這仍然面臨的是一個選擇的問題,HashSet底層也是使用數組實現的,它的優點是存取效率很高,當負載因子很小時,幾乎可以達到O(1)級的存取速度,但是它不是線程安全的。當我們需要在多線程併發環境下使用時可以考慮使用這個類,當然爲了實現線程安全,這不是一個唯一的方法。
Ø TreeSet的特點、實現機制及使用方法
a) TreeSet的特點:
TreeSet中所放的元素是有序的,並且元素是不能重複的。
b) TreeSet的實現機制:
TreeSet是如何保持元素的有序不重複的?
首先TreeSet底層使用TreeMap實現,和HashSet一樣,將每個要放入的元素放到key的位置,value位置放的是一個Object類型的常量。
在JDK源碼中有下面一段註釋:
- /**
- * Constructs a new, empty tree set, sorted according to the
- * natural ordering of its elements. All elements inserted into
- * the set must implement the {@link Comparable} interface.
- * Furthermore, all such elements must be <i>mutually
- * comparable</i>: {@code e1.compareTo(e2)} must not throw a
- * {@code ClassCastException} for any elements {@code e1} and
- * {@code e2} in the set. If the user attempts to add an element
- * to the set that violates this constraint (for example, the user
- * attempts to add a string element to a set whose elements are
- * integers), the {@code add} call will throw a
- * {@code ClassCastException}.
- */
從註釋中可以看出保證不重複的關鍵因素不是hashCode和equals方法,而是compareTo。也就是說要加入的元素要實現Comparable接口。
c) TreeSet的使用方法:
在總結HashSet的使用方法時,我們用到了一個例子,那麼在使用TreeSet時同樣是一個選擇的問題,我們是否要保證插入的元素有序(不是按插入順序有序,而是根據compareTo的返回值排序)是我們選擇使用那種類型的Set的一個標準。(我不是專家,我只是菜鳥,歡迎拍磚)
Ø ConcurrentSkipListSet的特點、實現機制及使用方法
a) ConcurrentSkipListSet的特點:
首先必須說的是這個類的名字很是讓我奇怪,就像我當時奇怪CopyOnWriteArrayList一樣,覺得這是一個比較長的名字,但是當我查了Copy-on-Write的意思時我就不再奇怪了,甚至讓我猜到了它的實現機制。
那麼Concurrent-Skip是什麼意思呢?並行跳過?
與大多數其他併發 collection 實現一樣,此類不允許使用 null 元素,因爲無法可靠地將 null 參數及返回值與不存在的元素區分開來。
b) ConcurrentSkipListSet的實現機制:
ConcurrentSkipListSet底層是使用ConcurrentSkipListMap實現的。那麼並行跳過到底是什麼意思,本人暫時不能做出總結。⊙﹏⊙b汗
c) ConcurrentSkipListSet的使用方法: