Set集合與Collection基本相同,沒有提供任何額外的方法,實際上Set就是Collection,隻是不允許包含相同的元素。
HashSet類
HashSet是Set接口的典型實作,按Hash算法來存儲集合中的元素,具有很好的存取和查找性能。
HashSet具有以下特點:不能保證元素的排列順序,順序可能與添加順序不同,順序也有可能發生變化。HashSet不是同步的,如果多個線程同時通路一個HashSet,假如有兩個或兩個以上線程同時修改了HashSet集合時,則必須通過代碼來保證其同步。集合元素值可以是null。
當向HashSet集合中存入一個元素時,HashSet會調用該對象的hashCode()方法來得到該對象的hashCode值,然後根據hashCode值決定該對象在HashSet中的存儲位置。如果有兩個元素通過equals方法比較傳回true,但它們的hashCode方法傳回值不相等,HashSet将會把它們存儲在不同的位置,依然可以添加成功。HashSet集合判斷兩個元素相等的标準是兩個對象通過equals方法比較相等,并且兩個對象的hashCode方法傳回值也相等。
注意點:當把一個對象放入HashSet中時,如果需要重寫該對象對應類的equals方法,則也應該重寫其hashCode方法。規則是,如果兩個對象通過equals方法比較傳回true,這兩個對象的hashCode值也應該相同。如果兩個對象通過equals方法比較傳回true,但這兩個對象的hashCode方法傳回不同的hashCode值時,這将導緻HashSet會把這兩個對象儲存在Hash表的不同位置,進而使兩個對象都可以添加成功,這就與Set集合的規則沖突了。如果兩個對象的hashCode方法傳回的hashCode值相同,但它們通過equals方法比較傳回fasle時将更麻煩,因為兩個對象的hashCode值相同,HashSet将試圖把它們儲存在同一位置,但又不行(否則将隻剩下一個對象),是以實際上會在這個位置用鍊式結構來儲存多個對象;而HashSet通路集合元素時也是根據元素的hashCode值來快速定位的,如果HashSet中兩個以上的元素具有相同的hashCode值,将會導緻性能下降。
HashSet中每個能存儲元素的槽位slot通常稱為桶bucket,如果有多個元素的hashCode值相同,但它們通過equals()方法比較傳回false,就需要在一個桶裡放多個元素,這樣會導緻性能下降。
重寫hashCode方法的一般步驟:
-
把對象内每個有意義的執行個體變量(即每個參與equals方法比較标準的執行個體變量)計算出一個int類型的hashCode值。
| 執行個體變量 | 計算方式 |
| 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(); |
-
用第1步計算出來的多個hashCode值組合計算出一個hashCode值傳回。
例如
為了避免直接相加産生偶然相等(兩個對象的f1、f2執行個體變量并不相等,但它們的hashCode的和恰好相等),可通過為各執行個體變量的hashCode乘以任意一個質數後再相加return f1.hashCode() + (int)f2;
return f1.hashCode()*19 + (int)f2*31;
class R
{
int count;
public R(int count) { this.count = count; }
public String toString() { return "R[count:"+count+"]"; }
public boolean equals(Object obj)
{
if(this == obj) return true;
if(obj != null && obj.getClass() == R.class){
R r = (R)obj;
return this.count == r.count;
}
return false;
}
public int hashCode() { return this.count; }
}
public class HashSetTest2
{
public static void main(String[] args)
{
HashSet hs = new HashSet();
hs.add(new R(5));
hs.add(new R(-3));
hs.add(new R(9));
hs.add(new R(-2));
// 列印HashSet集合,集合元素沒有重複
System.out.println(hs);
// 取出第一個元素
Iterator it = hs.iterator();
R first = (R)it.next();
// 為第一個元素的count執行個體變量指派
first.count = -3;
// 再次輸出HashSet集合,集合元素有重複元素
System.out.println(hs);
// 删除count為-3的R對象
hs.remove(new R(-3));
// 可以看到被删除一個R元素
System.out.println(hs);
System.out.println("hs是否包含count為-3的R對象?"+hs.contains(new R(-3))); // 輸出false
System.out.println("hs是否包含count為-2的R對象?"+hs.contains(new R(-2))); // 輸出false
}
}
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5SO2czN1QWM3Y2Y4gjY0YGZyYzXyUzMwATM2IzLcdDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
LinkedHahSet類
LinkedHashSet集合根據元素的hashCode值來決定元素的存儲位置,但它同時使用連結清單維護元素的次序,這樣使得元素看起來是以插入的順序儲存的。也就是說,當周遊LinkedHasdSet集合裡的元素時,LinkedHashSet将會按元素的添加順序來通路集合裡的元素。
public class LinkedHashSetTest
{
public static void main(String[] args){
LinkedHashSet books = new LinkedHashSet();
books.add("瘋狂JAVA講義");
books.add("輕量級Java EE企業應用實戰");
System.out.println(books);
// 删除 瘋狂Java講義
books.remove("瘋狂JAVA講義");
// 添加 瘋狂Java講義
books.add("瘋狂JAVA講義");
System.out.println(books);
}
}
雖然LinkedHashSet使用了連結清單記錄集合元素的添加順序,但LinkedHashSet依然是HashSet,是以它依然不允許集合元素重複。
TreeSet類
TreeSet是SortedSet接口的實作類,可以確定集合元素處于排序狀态,根據元素實際值的大小來進行排序的。TreeSet采用紅黑樹的資料結構來存儲集合元素,并且支援兩種排序方法:自然排序和定制排序。
TreeSet nums = new TreeSet();
// 向TreeSet中添加四個Integer對象
nums.add(5);
nums.add(2);
nums.add(10);
nums.add(-9);
// 輸出集合元素,看到集合元素已經處于排序狀态
System.out.println(nums);
// 輸出集合中第一個元素
System.out.println(nums.first());
// 輸出集合中最後一個元素
System.out.println(nums.last());
// 傳回小于4的子集,不包括4
System.out.println(nums.headSet(4));
// 傳回大于5的子集,如果Set中包含5,子集中還包含5
System.out.println(nums.tailSet(5));
// 傳回大于等于-3、小于4的子集
System.out.println(nums.subSet(-3,4));
自然排序
TreeSet會調用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然後将集合元素按升序排列。Java提供了一個Comparable接口,該接口裡定義了一個compareTo(Object obj)方法,該方法傳回一個整數值,實作該接口的類必須實作該方法,實作了該接口的類對象就可以比較大小。當一個對象調用該方法與另一個對象進行比較時,例如obj1.compareTo(obj2),如果該方法傳回0,則表明這兩個對象相等;如果該方法傳回一個正整數,則表明obj1大于obj2;如果該方法傳回一個負整數,則表明obj1小于obj2。
Java 9改進了TreeSet實作,如果采用自然排序的Set集合的元素沒有實作Comparable接口,程式就會立即引發ClassCastException異常。大部分類在實作compareTo(Object obj)方法時,都需要将被比較對象obj強制類型轉換成相同類型,是以隻有相同類的兩個執行個體才會比較大小。當試圖把一個對象添加到TreeSet集合時,TreeSet會調用該對象的compareTo(Object obj)方法與集合中的其他元素進行比較-這就要求集合中的其他元素與該元素是同一個類的執行個體。也就是說,向TreeSet中添加的應該是同一個類的對象,否則會引發ClassCastException異常。如果向TreeSet中添加的對象是程式員自定義類的對象,則可以向TreeSet中添加多種類型的對象,前提是使用者自定義類實作了Comparable接口,且實作compareTo(Object obj)方法沒有進行強制類型轉換。但當試圖取出TreeSet裡的集合元素時,不同類型的元素依然會發生ClassCastException異常。
當需要把一個對象放入TreeSet中,重寫該對象對應類的equals()方法時,應保證該方法與compareTo(Object obj)方法有一緻的結果,其規則是:如果兩個對象通過equals方法比較傳回true時,這兩個對象通過compareTo(Object obj)方法比較應傳回0。
class Z implements Comparable
{
int age;
public Z(int age) { this.age = age; }
//重寫equals方法,總是傳回true
public boolean equals(Object obj) { return true; }
//重寫compareTo方法,總是傳回1
public int compareTo(Object obj) { return 1; }
}
TreeSet set = new TreeSet();
Z z1 = new Z(6);
set.add(z1);
//第二次添加同一個對象,輸出true,表示添加成功
//TreeSet依靠對象的compareTo與容器中其他對象比較大小,而自定義的Z一直傳回正數,表明不相等,可以添加
System.out.println(set.add(z1));
// 可見有兩個對象,其實是兩個引用,指向同一個對象
System.out.println(set);
((Z)(set.first())).age = 9;
System.out.println(((Z)(set.last())).age);
向TreeSet中添加一個可變對象後,通過修改該可變對象的執行個體變量,将導緻它與其他對象的大小順序發生改變,但TreeSet不會再次調整它們的順序。一旦改變了TreeSet集合裡可變元素的執行個體變量,當再試圖删除該對象時,TreeSet會删除失敗(甚至集合中原有的、執行個體變量沒被修改但與修改後元素相等的元素也無法删除),但是可以删除沒有被修改執行個體變量、且不與其他被修改執行個體變量的對象重複的對象,删除後TreeSet會對集合中的元素重新索引(不是重新排序),接下來就可以删除TreeSet中的所有元素了,包括那些被修改過執行個體變量的元素。與HashSet類似,如果TreeSet中包含了可變對象,當可變對象的執行個體變量被修改時,TreeSet在處理這些對象時将非常複雜,而且容易出錯。
import java.util.TreeSet;
class R implements Comparable
{
int count;
public R(int count) { this.count = count; }
public String toString() { return "R[count:"+count+"]"; }
// 重寫equals方法,根據count來判斷是否相等
public boolean equals(Object obj)
{
if(this == obj){
return true;
}
if(obj != null && obj.getClass() == R.class)
{
R r = (R)obj;
return r.count == this.count;
}
return false;
}
// 重寫compareTo方法,根據count來比較大小
public int compareTo(Object obj){
R r = (R)obj;
return count > r.count ? 1 : count < r.count ? -1 : 0;
}
}
public class TreeSetTest {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new R(5));
ts.add(new R(-3));
ts.add(new R(9));
ts.add(new R(-2));
// 列印TreeSet集合,集合元素是有序排列的
System.out.println(ts);
// 取出第一個元素
R first = (R)ts.first();
// 對第一個元素的count指派
first.count = 20;
// 取出最後一個元素
R last = (R)ts.last();
// 對最後一個元素的count指派,與第二個元素的count相同
last.count = -2;
// 再次輸出将看到TreeSet裡的元素處于無序狀态,且有重複元素
System.out.println(ts);
// 删除執行個體變量被改變的元素,删除失敗
System.out.println(ts.remove(new R(-2)));
System.out.println(ts);
// 删除執行個體變量沒有被改變的元素,删除成功
System.out.println(ts.remove(new R(5)));
System.out.println(ts);
}
}
定制排序
如果需要實作定制排序,則需要在建立TreeSet集合對象時,提供一個Comparator對象與該TreeSet集合關聯,由該Comparator對象負責集合元素的排序邏輯。由于Comparator是一個函數式接口,是以可使用Lambda表達式來代替Comparator對象。
當通過Comparator對象(或Lambda表達式)來實作TreeSet的定制排序時,依然不可以向TreeSet中添加類型不同的對象,否則會引發ClassCastException異常。使用定制排序時,TreeSet對集合元素排序不管集合元素本身大小,而是由Comparator對象(或Lambda表達式)負責集合元素的排序規則。TreeSet判斷兩個集合元素相等的标準是:通過Comparator(或Lambda表達式)比較兩個元素傳回了0,這樣TreeSet不會把第二個元素添加到集合中。
import java.util.TreeSet;
class M
{
int age;
public M(int age) { this.age = age; }
public String toString() { return "M[age:"+age+"]"; }
}
public class TreeSetTest1 {
public static void main(String[] args) {
// Lambda表達式的目标類型是Comparator
TreeSet ts = new TreeSet((o1, o2) -> {
M m1 = (M)o1;
M m2 = (M)o2;
// 根據M對象的age屬性來決定大小,age越大,M對象反而越小
return m1.age > m2.age ? -1 : m1.age < m2.age ? 1 : 0;
});
ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
ts.add(new M(-2));
System.out.println(ts);
}
}
EnumSet類
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
enum Season
{
SPRING,SUMMER,FALL,WINTER
}
public class EnumSetTest {
public static void main(String[] args) {
// 建立一個EnumSet集合,集合元素就是Season枚舉類的全部枚舉值
EnumSet es1 = EnumSet.allOf(Season.class);
System.out.println(es1);
// 建立一個EnumSet空集合,指定其集合元素是Season類的枚舉值
EnumSet es2 = EnumSet.noneOf(Season.class);
System.out.println(es2);
// 手動添加兩個元素
es2.add(Season.WINTER);
es2.add(Season.SPRING);
System.out.println(es2);
// 以指定枚舉值建立EnumSet集合
EnumSet es3 = EnumSet.of(Season.SUMMER, Season.WINTER);
System.out.println(es3);
EnumSet es4 = EnumSet.range(Season.SUMMER, Season.WINTER);
System.out.println(es4);
// 新建立的EnumSet集合元素和es4集合元素有相同的類型
// es5集合元素 + es4集合元素 = Season枚舉類的全部枚舉值
EnumSet es5 = EnumSet.complementOf(es4);
System.out.println(es5);
Collection c = new HashSet();
c.clear();
c.add(Season.FALL);
c.add(Season.SPRING);
// 複制Collection集合中的所有元素來建立EnumSet集合
EnumSet enumSet = EnumSet.copyOf(c);
System.out.println(enumSet);
}
}