国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

Java集合框架之Set

2019-11-06 08:12:29
字體:
來源:轉載
供稿:網友

1.Set

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

Set集合定義的方法

Set集合不允許包含相同的元素,如果試圖把兩個相同的元素加入同一個Set集合中,則添加操作失敗,add()方法返回false,且新元素并不會被加入。

2.HashSet

HashSetSet接口的典型實現,大多數的時候使用Set集合時就是使用這個實現類。HashSet按Hash算法來存儲集合元素,因此具有很好的存取和查找性能。 HashSet具有以下特點:

1.不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發生變化2.HashSet不是同步的,如果多個線程同時訪問一個HashSet,假設兩個或兩個以上線程同時修改了HashSet集合時,則必須通過代碼來保證其同步。否則很可能會出現異常。3.集合元素可以是null

2.1 判斷元素是否相同的標準

當向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對象當成同一個對象,運行上面的程序,可以看到如下結果:

[B@1, B@1, C@2, A@2a139a55, A@15db9742]

可以看到,即使兩個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散列值有沖突的情況,雖然概率很低)

2.2 hashCode值的計算方式

通過前面的介紹,我們可以看到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,shortcharint 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無法正確操作這些集合元素!

3.LinkedHashSet

HashSet有一個子類是LinkedHashSet,它的用法與HashSet基本一致,LinkedHashSet集合也是根據元素的hashCode值來決定元素的存儲位置,但它同時使用鏈表維護元素的次序,這樣使得元素看起來是以插入的順序來保存的。也就是說,當遍歷LinkedHashSet集合里的元素時,將會按照元素的添加順序來訪問集合里的元素。

LinkedHashSet需要維護元素的插入順序,因此性能略低于HashSet的性能,但在迭代訪問Set`里的全部元素時將有很好的性能,因為它以鏈表來維護內部順序。

public static void main(String[] args) { LinkedHashSet<String> set = new LinkedHashSet<>(); set.add("Gavin"); set.add("Java"); set.add("Python"); set.add("C++"); //可以看到,LinkedHashSet的輸出是按照添加順序的 // [Gavin, Java, Python, C++] System.out.println(set);}

4.TreeSet

4.1 基本概念

TreeSetSortedSet接口的實現類,正如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));}

4.2 排序方法

HashSet集合采用hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數據結構來存儲集合元素。它支持兩種排序方法:自然排序和定制排序。在默認情況下,TreeSet采用自然排序。

4.2.1 自然排序

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()方法:

public class Person implements Comparable { private String name; private int age; private float weight; private float height; public Person(String name) { super(); this.name = name; } public Person(String name, int age, float weight, float height) { super(); this.name = name; this.age = age; this.weight = weight; this.height = height; } @Override public boolean equals(Object obj) { // 測試兩個對象是否是同一個對象,是的話返回true if (this == obj) { return true; } // 測試檢測的對象是否為空,是null就返回false if (obj == null) { return false; } // 測試兩個對象所屬的類是否相同,否則返回false if (this.getClass() != obj.getClass()) { return false; } // 將obj轉化為Person對象,以便和Person類的對象進行比較 Person p = (Person) obj; return this.name.equals(p.name) && this.age == p.age && this.weight == p.weight && this.height == p.height; } @Override public int hashCode() { return this.name.hashCode() * 3 + this.age * 7 + Float.floatToIntBits(this.height) * 11 + Float.floatToIntBits(this.weight) * 13; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + ", weight=" + weight + ", height=" + height + "]"; } @Override public int compareTo(Object o) { return this.name.compareTo(((Person) o).name); }}

從上述代碼可以看到,compareTo(Object obj)方法的比較標準是對象name屬性的比較標準,也就是按照name屬性字符串中字符的Unicode值進行比較,而equals()方法是按照四個屬性是否相等來比較的。接著我們再做如下測試:

public class Test { public static void main(String[] args) { Person p1 = new Person("Jack", 18, 78.5f, 178.5f); Person p2 = new Person("Jack", 19, 73.5f, 172.5f); Person p3 = new Person("Anna", 20, 55f, 165f); System.out.println(p1.equals(p2)); TreeSet<Person> set = new TreeSet<>(); set.add(p1); set.add(p2); // p2是放不進去的 set.add(p3); // 使用lambda遍歷輸出 set.forEach(obj -> System.out.println(obj)); }}

程序輸出:

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)方法的關鍵實例變量!

4.2.2 定制排序

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對象,如下:

public class Test { public static void main(String[] args) { TreeSet<Integer> set2 = new TreeSet<Integer>(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { // 這里的負號即可將排序方式反轉 return -o1.compareTo(o2); }; }); set2.add(1); set2.add(8); set2.add(4); System.out.println(set2); }}

程序輸出:

[8, 4, 1]

5.Set的遍歷

對于所有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());}
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 山阳县| 烟台市| 留坝县| 阿拉善右旗| 息烽县| 汕尾市| 黎川县| 手游| 区。| 法库县| 土默特右旗| 思茅市| 丹寨县| 卓资县| 凉城县| 泰顺县| 庆阳市| 武强县| 沿河| 四子王旗| 连江县| 阿拉善右旗| 天气| 岑溪市| 承德县| 靖宇县| 桂阳县| 永胜县| 江安县| 鄂温| 邓州市| 诏安县| 蒙城县| 延津县| 林芝县| 孟州市| 新郑市| 盱眙县| 宁津县| 天门市| 广宁县|