Set集合,它類似于一個罐子,程序可以依次把多個對象放進Set集合,而Set集合通常不能記住元素的添加順序。Set集合與Collection提供的方法基本相同,它定義的方法如下:

Set集合不允許包含相同的元素,如果試圖把兩個相同的元素加入同一個Set集合中,則添加操作失敗,add()方法返回false,且新元素并不會被加入。
HashSet是Set接口的典型實現,大多數的時候使用Set集合時就是使用這個實現類。HashSet按Hash算法來存儲集合元素,因此具有很好的存取和查找性能。 HashSet具有以下特點:
HashSet不是同步的,如果多個線程同時訪問一個HashSet,假設兩個或兩個以上線程同時修改了HashSet集合時,則必須通過代碼來保證其同步。否則很可能會出現異常。3.集合元素可以是null當向HashSet集合中存入一個元素時,HashSet會調用該對象的hashCode()方法來得到該對象的hashCode值,然后根據該hashCode值決定該對象在HashSet中的存儲位置。
如果有兩個元素通過equals()方法比較返回true,但它們的hashCode()方法返回值不相等,HashSet將會把它們存儲在不同的位置,依然可以添加成功。
也就是說,HashSet集合判斷兩個元素相等的標準是兩個對象的hashCode()方法返回值相等,并且兩個對象通過equals()方法比較也相等
下面舉一個例子:
import java.util.HashSet;class A { /** * Class A 的equals()方法總是返回true,但是沒有重寫其hashCode()方法 */ @Override public boolean equals(Object obj) { return true; }}class B { /** * Class B 的hashCode()方法總是返回1,但是沒有重寫其equals()方法 */ @Override public int hashCode() { return 1; }}class C { /** * Class C 的hashCode()方法總是返回2,且重寫其equals()方法總是返回true */ @Override public int hashCode() { return 2; } @Override public boolean equals(Object obj) { return true; }}public class HashSetTest { public static void main(String[] args) { HashSet sets = new HashSet(); sets.add(new A()); sets.add(new A()); sets.add(new B()); sets.add(new B()); sets.add(new C()); sets.add(new C()); System.out.PRintln(sets); }}上面的程序定義了A、B、C三個類,它們分別重寫了equals()、hashCode()兩個方法的一個或全部。并且在主方法中,向sets集合中添加了兩個A對象,兩個B對象和兩個C對象,其中C類重寫了equals()方法總是返回true,hashCode()方法總是返回2,這將導致HashSet把兩個C對象當成同一個對象,運行上面的程序,可以看到如下結果:
可以看到,即使兩個A對象通過equals()方法比較返回true,但HashSet仍然把它們當成兩個對象;即使兩個B對象的hashCode()方法返回值相同,都是1,但HashSet也把它們當成兩個對象。
從上面的例子,我們可以得到這樣的結論:
當把一個自定義對象放入HashSet中時,如果需要重寫該對象對應類的equals()方法,則也應該重寫其hashCode()方法。規則是:如果兩個對象通過equals()方法比較返回true,這兩個對象的hashCode值也應該相同。
如果兩個對象通過equals()方法比較返回true,但這兩個對象的hashCode()方法返回不同的hashCode值時,這將導致HashSet會把這兩個對象保存在Hash表的不同位置,從而使兩個對象都可以添加成功,這就與Set集合的規則沖突了。
如果兩個對象的hashCode()方法返回的hashCode值相同,但它們通過equals()方法比較返回false時,這種情況更為麻煩:因為兩個對象的hashCode值相同,HashSet將試圖把它們保存在同一個位置,但又不行(否則將會只剩下一個對象),所以實際上,會在這個位置上用鏈式結構來保存多個對象;而HashSet訪問集合元素時也是根據元素的hashCode值來快速定位的,如果HashSet中兩個以上的元素具有相同的hashCode值,這將會導致性能下降。
HashSet中每個能存儲元素的“槽位”(slot)通常稱為“桶”(bucket),如果有多個元素的hashCode值相同,但它們通過equals()方法比較返回false,就需要在一個“桶”里放多個元素,這樣會導致性能的降低。
總結一下:
在集合中,判斷兩個對象是否相等的規則是: (1)第一步,判斷hashCode()是否相等,如果hashCode()相等,則查看第二步,否則不相等。 (2)第二步,判斷equals()是否相等,如果相等,則兩obj相等,否則還是不相等。
所以: (1)兩個obj,如果equals()相等,則hashCode()一定相等。(否則Set集合就亂套了) (2)兩個obj,如果hashCode()相等,equals()不一定相等。(Hash散列值有沖突的情況,雖然概率很低)
通過前面的介紹,我們可以看到hashCode()方法對于HashSet集合的重要性。那么如果我們要重寫一個類的hashCode()方法,我們該怎么著手呢?
重寫hashCode()方法有三個基本原則:
1.在程序運行過程中,同一個對象多次調用hashCode()方法應該返回相同的值 2.當兩個對象通過equals()方法比較返回true時,這兩個對象的hashCode()方法應該返回相等的值。 3.對象中用作equals()方法比較標準的實例變量,都應該用于計算hashCode值。
重寫hashCode()的一般步驟如下:
1.把對象內每個有意義的實例變量(即每個參與equals()方法比較標準的實例變量)計算出一個int類型的hashCode值。 計算方式如下(假設實例變量為f):
| 實例變量類型 | 計算方式 |
|---|---|
boolean | hashCode = (f ? 0 : 1); |
整數類型(byte,short,char,int) | hashCode = (int)f; |
long | hashCode = (int)(f^(f>>>32)); |
float | hashCode = Float.floatToIntBits(f); |
double | long L = Double.doubleToLongBits(f); hashCode = (int)(L ^ (L >>> 32)); |
| 引用類型 | hashCode = f.hashCode() |
2.用第1步計算出來的多個hashCode值組合計算出一個hashCode值返回。 比如如下,直接進行簡單的相加:
return f1.hashCode() + (int)(f2 ^ (f2 >>> 32));當然,為了避免直接相加產生偶然的相等,可以通過為各實例變量的hashCode值乘以任意一個質數后再相加。如下:
return f1.hashCode() * 17 + (int)(f2 ^ (f2 >>> 32)) * 31;一定要注意的是:當程序把可變對象添加到HashSet中之后,盡量不要去修改該集合元素中參與計算hashCode()和equals()方法的實例變量,否則可能會導致HashSet無法正確操作這些集合元素!
HashSet有一個子類是LinkedHashSet,它的用法與HashSet基本一致,LinkedHashSet集合也是根據元素的hashCode值來決定元素的存儲位置,但它同時使用鏈表維護元素的次序,這樣使得元素看起來是以插入的順序來保存的。也就是說,當遍歷LinkedHashSet集合里的元素時,將會按照元素的添加順序來訪問集合里的元素。
LinkedHashSet需要維護元素的插入順序,因此性能略低于HashSet的性能,但在迭代訪問Set`里的全部元素時將有很好的性能,因為它以鏈表來維護內部順序。
TreeSet是SortedSet接口的實現類,正如SortedSet名字所暗示的,TreeSet可以確保集合元素處于排序狀態。它使用紅黑樹算法來維護集合元素的次序。
與HashSet集合相比,TreeSet還提供了如下幾個額外的方法:
| 方法 | 說明 |
|---|---|
Comparator comparator() | 如果TreeSet采用了定制排序,則該方法返回定制排序所使用的Comparator;如果TreeSet采用了自然排序,則返回null |
Object first() | 返回集合中的第一個元素 |
Object last() | 返回集合中最后一個元素 |
Object lower(Object e) | 返回集合中位于指定元素之前的元素(即小于指定元素的最大元素,參考元素不需要是TreeSet集合里的元素) |
Object higher(Object e) | 返回集合中位于指定元素之后的元素(即大于指定元素的最大元素,參考元素不需要是TreeSet集合里的元素) |
SortedSet subSet(Object fromElement, Object toElement) | 返回此Set的子集合,范圍從fromElement(包含)到toElement(不包含) |
SortedSet headSet(Object toElement) | 返回此Set的子集,由小于toElement的元素組成 |
SortedSet tailSet(Object fromElement) | 返回此Set的子集,由大于fromElement的元素組成 |
下面是一個測試程序:
public static void main(String[] args) { TreeSet nums = new TreeSet<>(); nums.add(5); nums.add(2); nums.add(10); nums.add(-9); // 輸出集合元素,可以看到集合已經排好了序,[-9, 2, 5, 10] System.out.println(nums); // 輸出集合的第一個元素,-9 System.out.println(nums.first()); // 輸出集合的最后一個元素,10 System.out.println(nums.last()); // 返回小于5的子集,不包含5,[-9, 2] System.out.println(nums.headSet(5)); // 返回大于等于5的子集,[5, 10] System.out.println(nums.tailSet(5)); // 返回大于等于-3,小于4的子集,[2] System.out.println(nums.subSet(-3, 4));}與HashSet集合采用hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數據結構來存儲集合元素。它支持兩種排序方法:自然排序和定制排序。在默認情況下,TreeSet采用自然排序。
TreeSet會調用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然后將集合按照升序排列,這就是自然排序。
(1)Java提供了一個Comparable接口,該接口定義了一個compareTo(Object obj)方法,該方法返回一個整數值,實現該接口的類必須實現該方法,實現了該接口的類的對象就可以比較大小。 (2)當一個對象調用該方法與另一個對象進行比較時,例如obj1.compareTo(obj2),如果該方法返回0,則表明這兩個對象是相等的;如果該方法返回一個正整數,則表明obj1大于obj2;如果該方法返回一個負整數,則表明obj1小于obj2。
Java的一些常用類已經實現了Comparable接口,并提供了比較大小的標準。例如:
| 類 | 說明 |
|---|---|
| BigDecimal、BigInteger以及所有的數值類對應的包裝類 | 按照它們對應的數值大小進行比較 |
| Character | 按字符的Unicode值進行比較 |
| Boolean | true對應的包裝類實例大于false對應的包裝類實例 |
| String | 按字符串中字符的Unicode值進行比較 |
| Date、Time | 后面的時間和日期比前面的時間和日期大 |
當一個對象加入TreeSet集合中時,TreeSet調用該對象的compareTo(Object obj)方法與容器中的其他對象比較大小,然后根據紅黑樹結果找到它的存儲位置。如果兩個對象通過compareTo(Object obj)方法比較相等,新對象將無法添加到TreeSet集合中。
由此應該注意到一個問題:當需要把一個對象放入TreeSet中,重寫該對象對應類的equals()方法時,應保證該方法與compareTo(Object obj)方法有一致的結果,其規則是:如果兩個對象通過equals()方法比較返回true時,這兩個對象通過compareTo(Object obj)方法比較應該返回0。
如果兩個對象通過compareTo(Object obj)方法比較返回0,但它們通過equals()方法比較返回false將會很麻煩,因為兩個對象通過compareTo(Object obj)方法比較相等,TreeSet不會讓第二個元素添加進去,這就會與Set集合的規則沖突。
我們可以做如下測試:
首先定義Person類,為了能夠將Person類放進TreeSet中,需要實現Comparable接口,并重寫其compareTo(Object obj)方法。還需要重寫其hashCode()方法和equals()方法:
從上述代碼可以看到,compareTo(Object obj)方法的比較標準是對象name屬性的比較標準,也就是按照name屬性字符串中字符的Unicode值進行比較,而equals()方法是按照四個屬性是否相等來比較的。接著我們再做如下測試:
程序輸出:
falsePerson [name=Anna, age=20, weight=55.0, height=165.0]Person [name=Jack, age=18, weight=78.5, height=178.5]p1和p2的name屬性一樣,都是”Jack”,但是其他屬性都不一樣。也就是equals()方法會返回false,但是p2卻不能添加到我們定義的set中了,因為compareTo(Object obj)方法返回了true。
我們可以總結出如下結論:
(1)如果試圖把一個對象添加到TreeSet中,則該對象的類必須實現Comparable接口,并重寫其compareTo(Object obj)方法,否則程序將會拋出異常。 (2)如果希望TreeSet能夠正常運行,TreeSet中只能添加同一種類型的對象。這樣才能保證compareTo(Object obj)方法正常工作,否則會出現異常。 (3)要注意equals()方法與compareTo(Object obj)方法的規則 (4)與HashSet類似,如果要保證程序不出錯,當對象放入TreeSet后,不要修改參與計算equals()方法和compareTo(Object obj)方法的關鍵實例變量!
TreeSet的自然排序是根據集合元素的大小,將它們按照升序進行排列。如果需要實現定制排序,例如以降序排列,則可以通過Comparator接口的幫助。該接口包含一個int compare(T o1, T o2)方法,該方法用于比較o1和o2的大小:如果該方法返回正整數,則表明o1大于o2;如果該方法返回0,則表明o1等于o2;如果該方法返回負整數,則表明o1小于o2。
如果需要實現定制排序,我們需要在創建TreeSet集合對象時,提供一個Comparator對象與該TreeSet集合關聯,由該Comparator對象負責集合元素的排序邏輯。這時候,即使TreeSet中的對象對應的類實現了Comparable接口,在這里也不起作用了,或者干脆不實現Comparable接口也沒有關系。
這里舉一個例子,比如我們要將一個TreeSet<Integer>按照降序排列,則可以給該TreeSet提供一個自定義的Comparator對象,如下:
程序輸出:
[8, 4, 1]對于所有Set集合的遍歷,很顯然,我們不能用普通的for循環,因為Set集合并不支持通過下標獲取元素的Object get(index)方法。
我們可以采用foreach循環或者迭代器Iterator的方式:
TreeSet<Integer> set = new TreeSet<Integer>();set.add(1);set.add(2);set.add(3);for(Integer i : set){ System.out.println(i);}Iterator<Integer> it = set.iterator();while(it.hasNext()){ System.out.println(it.next());}新聞熱點
疑難解答