Set集合
Set集合類似於一個罐子,不記錄添加元素的添加順序,只是不允許包含重復元素(重復的判定在不同的實現類中可能有些區別。
HashSet類
HashSet具有很好的存取和查找性能。
HashSet有以下特點:
- 不能保證元素的排列順序,順序可能與添加順序不同,順序也可能發生變化
- HashSet 不是同步的,如果多個線程訪問同一個HashSet ,並且有兩個或兩個以上的線程同時修改HashSet,則必須通過代碼來保證同步
- 集合元素值可以是null
Set像是一個罐子,記不住添加元素的順序,所以查找的時候只能根據元素本身的屬性去查找,因此Set不允許包含重復元素(這個重復的判定在不同的實現類中可能有細微的差別)
import java.util.HashSet;
class A{
@Override
public boolean equals(Object obj) {
return true;
}
@Override
public int hashCode() {
return 1;
}
}
class B{
@Override
public boolean equals(Object obj) {
return true;
}
}
class C{
@Override
public int hashCode() {
return 2;
}
}
public class HashSetTest {
public static void main(String[] args) {
HashSet s = new HashSet();
System.out.println(s.add(null));
System.out.println(s.add(null));
s.forEach(ele-> System.out.println(ele));
HashSet h = new HashSet();
System.out.println(h.add(new A()));
System.out.println(h.add(new A()));
System.out.println(h.add(new B()));
System.out.println(h.add(new B()));
System.out.println(h.add(new C()));
System.out.println(h.add(new C()));
System.out.println(h);
}
}
輸出結果
true
false
null
true //可以看出 HashSet集合判斷兩個元素相等的標准是:兩個對象通過equals()方法比較得到true,並且hashCode()方法的返回值也相等
false
true //equals()返回true,但hashCode()返回值不相等時,會把它們保存在不同位置,但這違反了Set集合的“不包含重復元素”的規則
true
true //equals()返回false,但hashCode()返回值相等時,會把它們用鏈式結構保存在一個位置,這樣會導致在訪問時性能下降
true //(兩個以上的元素具有相同的hashCode值)
[A@1, C@2, C@2, B@54a097cc, B@36f6e879]
基於hashCode()和equals()方法對於HashSet的重要性,對於HashSet中的對象,要遵循以下重寫這兩個方法的基本規則:
- hashCode()每次運行的返回值應該一樣
- 當兩個對象通過equals()方法返回true時,這兩個對象的hashCode()方法應該返回相同的值
- 對象中用作equals()方法比較標准的實例變量,都應該用於計算hashCode值
重寫案例
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
class R{
Double ooo;
public R(double d){
this.ooo = d;
}
@Override
public String toString() {
return "R{" +
"ooo=" + ooo +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
R r = (R) o;
return Objects.equals(ooo, r.ooo);
}
@Override
public int hashCode() {
long l = Double.doubleToLongBits(this.ooo);
return (int)(1^(l>>>32));
}
}
public class HashSetTest2 {
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add(new R(1.1));
// 整一個迭代器,把第一個元素取出來,沒有進入迭代過程所以可以修改元素
// (這也算不上修改吧,迭代過程中也能修改元素,
// 但除了迭代器的remove()方法外不能用其他方法添加或刪減元素)
Iterator it = hs.iterator();
R first = (R)it.next();
first.ooo = 2.2;
System.out.println(hs);
// contains(Object o)方法:HashSet集合會先得到o的hashCode,然后到自己集合中對應的地方去找,
// 找到后在通過equals()方法去比較,得到true后返回
System.out.println(hs.contains(new R(1.1)));//因為equals返回了false
System.out.println(hs.contains(new R(2.2)));//壓根就沒找到R(2.2)對應的hashCode
/*
由此可見,對於已經添加進去的對象,盡量不要修改關於該元素參與計算hashCode()和equals()的實例變量,
否則,像上面的hs中的那一個對象,既無法通過contains(1.1)訪問到,也無法通過contains(2.2)訪問到
這樣會給集合操作這些元素帶來很大麻煩
*/
}
}
輸出結果
[R{ooo=2.2}]
false
false
LinkedHashSet類
相對於HashSet的區別:
LinkedHashSet 是HashSet的子類,LinkedHashSet集合也是根據元素的hashCode值來決定元素的存儲位置,但它同時維護元素存儲的次序。
當遍歷LinkedHashSet集合里的元素時,LinkedHashSet將會按元素的添加順序來訪問集合中的元素。
import java.util.LinkedHashSet;
public class LinkedHashSetTest {
public static void main(String[] args) {
LinkedHashSet ls = new LinkedHashSet();
ls.add("a");
ls.add("b");
ls.add("c");
ls.add("d");
System.out.println(ls);
ls.remove("c");
System.out.println(ls);
}
}
[a, b, c, d]
[a, b, d]
此實現使客戶擺脫了HashSet提供的未指定的,通常混亂的排序,而不會導致與TreeSet相關的增加的成本。無論原始集的實現如何,都可以使用它來產生與原始集具有相同順序的集合副本:
void foo(Set s) {
Set copy = new LinkedHashSet(s);
...
}
性能
由於需要維護元素的插入順序(其實是維護鏈式列表),LinkedHashSet相對於HashSet會增加開銷,因此執行添加、刪除等操作時性能會輕微下降,但當迭代的時候LinkedHashSet集合的迭代時間與集合的大小成正比,而HashSet集合的迭代時間與集合的容量成正比,因此LinkedHashSet的迭代性能要好一些。
當構造HashSet集合時,有兩個參數,一個是初始化容量(默認是16),一個是負載因子(默認值是0.75),因為HashSet的迭代性能和容量成正比,所以我們通常不會設置太大的容量,默認或者更低。
但當構造LinkedHashSet時,選擇初始化容量過高的值的副作用不會那么嚴重,因為其迭代性能與集合大小成正比。
TreeSet類
public class TreeSetTest {
public static void main(String[] args) {
TreeSet s = new TreeSet();
s.add(1);
s.add(2);
s.add(3);
s.add(4);
System.out.println(s);
System.out.println(s.first()); //1
System.out.println(s.last()); //4
System.out.println(s.headSet(3)); //1,2
System.out.println(s.tailSet(2)); //2,3,4
System.out.println(s.subSet(1,3)); //1,2,3
}
}
可以看出(好像也看不太出。。沒事),TreeSet並不是根據元素的插入順序進行排序的,而是根據元素的實際值大小來排序的
與HashSet集合采用hash算法來決定元素的存儲位置不同,TreeSet采用紅黑樹的數據結構來存儲集合元素。
TreeSet支持兩種排序規則:自然排序和定制排序。在默認情況下,TreeSet采用自然排序。
TreeSet自然排序
自然排序:TreeSet會調用集合元素的compareTo(Object obj)方法來比較元素之間的大小關系,然后將集合元素按升序排列。
a.compareTo(b)方法來自Comparable接口,該方法返回一個整數:
返回0:兩個對象相等
返回1:a大於b
返回-1:a小於b
Java的一些類已經實現了Comparable接口,並提供了比較大小的標准:
- BigDemical、BigInteger以及所有的數值型對應的包裝類:按他們的大小進行比較
- Character:按字符的Unicode值進行比較
- Boolean:true對應的包裝類實例大於false對應的包裝類實例
- String:按字符串中字符的Unicode值進行比較
- Data、Time:(后面的時間、日期) 比 (前面的時間、日期大)
//下面程序試圖向TreeSet集合中添加兩個Err對象,添加第二個對象時,TreeSet會調用
//該對象的compareTo(Object o)方法與集合中的其他元素進行比較——
//如果其對應類沒有實現Comparable接口,則會引發ClassCastException異常
TreeSet ts = new TreeSet();
ts.add(new Error());
ts.add(new Error());
/**
*而且,大部分類在實現compareTo(Object o)方法的時候,都需要將被比較的對象o強轉為相同類型,因為只有兩個相同類型才能比較大小。
*因此,向TreeSet里添加的應該是同一個類的對象,不然容易引發ClassCastException異常
**/
總結:如果希望TreeSet能夠正常工作,TreeSet集合中只能存同一類對象。
HashSet和TreeSet中可變元素的實例變量盡量不要修改(參與相等判斷的)
對於TreeSet而言,判斷兩個元素是否相等的唯一標准是:兩個對象通過compareTo(Object o)返回的整數是不是0。
import java.util.TreeSet;
class P implements Comparable{
int num;
public P(int num) {
this.num = num;
}
@Override
public String toString() {
return "P{" +
"num=" + num +
'}';
}
@Override
public int compareTo(Object o) {
P p = (P)o;
return this.num>p.num? 1: this.num==p.num?0:-1;
}
}
public class TreeSet2 {
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add(new P(1));
ts.add(new P(5));
ts.add(new P(7));
ts.add(new P(9));
System.out.println(ts);
P first = (P)ts.first();
first.num = 100;
P last = (P)ts.last();
last.num = 7;
System.out.println(ts);
System.out.println(ts.remove(new P(1)));
System.out.println(ts.remove(new P(100)));
System.out.println(ts);
System.out.println(ts.remove(new P(5)));//①
System.out.println(ts);
/**
* 添加后的元素如果實例變量被修改(而且這個實例變量能夠影響compareTo()方法的判斷,TreeSet也不會重新排序
* 反而可能會造成出現重復的元素,導致兩個都無法被刪除,給操作帶來困難
*
* 一旦修改了TreeSet集合里可變元素的實例對象,當再試圖刪除該對象時,TreeSet也會刪除失敗
* 甚至連集合中原有的、實例變量沒有被修改但與修改后的元素相等的元素也無法被刪除
*
* 說明TreeSet可以刪除沒有被修改實例變量、且不與其他修改實例變量的對象重復的對象
*
* 當執行上面①行代碼時,TreeSet會對集合中的元素重新索引(不是重新排序)。接下來
* 就可以刪除TreeSet中的其他元素了。包括那些被修改過的實例變量的元素。
*/
}
}
[P{num=1}, P{num=5}, P{num=7}, P{num=9}]
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
false
false
[P{num=100}, P{num=5}, P{num=7}, P{num=7}]
true
[P{num=100}, P{num=7}, P{num=7}]
與HashSet類似的是,如果TreeSet中包含了可變對象,當可變對象的實例變量被修改時,TreeSet在處理這些對象將非常復雜,而且容易出錯.為了讓程序更加健壯,不要修改放入HashSet和TreeSet集合中的關鍵實例變量(與CompareTo()方法相關的).
定制排序
TreeSet如果想要實現定制排序,則可以通過Comparator接口的幫助。該接口里包含一個int compare(Object o1,Object o2)方法,o1 == o2 返回0;o1>o2 返回1;o1<o2 返回-1;
通過提供一個Comparator對象與TreeSet關聯,由該Comparator對象負責集合的排序邏輯。
由於Comparator是一個函數式接口,因此可使用Lambda表達式來代替Comparator對象。
import java.util.TreeSet;
class N{
int age;
public N(int age) {
this.age = age;
}
}
class M{
int age;
public M(int age) {
this.age = age;
}
@Override
public String toString() {
return "M{" +
"age=" + age +
'}';
}
}
public class TreeSetTest {
public static void main(String[] args) {
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(9));
ts.add(new M(10));
ts.add(new M(1));
// ts.add(new N(90)); ClassCastException異常 所以還是不要添加不同類的對象
System.out.println(ts);
}
}
[M{age=10}, M{age=9}, M{age=5}, M{age=1}]
