java集合概述之Set
Abstract
- Java的集合主要有
Set、List、Queue和Map四種體系。 這四種體系都是接口不能直接用,但是在這四種體系下包含了很多的實現類是可以直接使用的。 - 集合類和定長數組的區別主要在於,定長數組不僅可以存儲基本數據類型還有對象,但是集合類只能存儲對象。這里的對象是指對象引用
- 所有的集合類都位於java.util包下,后來為了處理多線程環境下的並發安全問題,java5還在java.util.concurrent包下提供了一些多線程支持的集合類。
- java中集合類都是由兩個接口派生出來的——Collection和Map,其中
Set、Queue和List接口是Collection接口派生出來的子接口,他們分別代表了無序集合和有序集合。 Map接口的所有實現類用於保存具有映射關系的數據(關聯數組)
Collection接口
boolean add(Object o)失敗返回false。boolean addAll(Collection o)用於把集合c中的所有元素添加到指定集合中,失敗分會false。void clear()清除集合中所有元素,集合長度變成0。boolean contains(Object o)判斷集合里是否包含指定元素。boolean containsAll(Collection c)判斷集合中是否包含了集合c中的所有元素。boolean isEmpty()返回集合是否為空。Iterator iterator()返回一個iteratior對象,用於遍歷集合中的所有元素。boolean remove(Object o)刪除制定元素o,當集合中存在多個符合條件的元素,僅僅刪除第一個並返回trueboolean removeAll(Collection c)從集合中刪掉集合c中的所有元素,如果有小於等於一個刪除成功邊返回true。boolean retainAll(Collection c)將集合中不是集合c的所有元素刪除,如果有至少一個刪除成功范湖true。int size()返回集合中的元素的個數。Object[] toArray()將集合轉化為一個數組,所有的集合元素變成對應的數組元素。
1. Set
實際上Set就是Collection,因為本身就是其子接口。唯一的不同就是Set不允許包含重復元素。如果強行加入相同的元素,add()函數會返回false。 無序、不可重復的集合。
在Set接口下有三個實現類分別是:HashSet、TreeSet和EnumSet,其中HashSet還有一個子類LinkedHashSet。
1.1 HashSet類
HashSet是Set最常見的實現類。有以下特性:
- 不能保證添加順序,因為原理上HashSet是通過Hash算法來存儲元素的,所以不能確定順序。
- HashSet不是同步的,假如有多個線程同時修改了HashSet的值必須通過代碼來保證其同步
- 集合元素值可以為NULL。
原理:
- 重點一:添加的元素只能是對象。
- 重點二:對象需要重寫
equals()函數和HashCode()函數。
根據重點有:
- 向HashSet集合中添加一個元素的時候,首先判斷是不是和集合中的某一個元素相等。
- 判斷相等的標准是兩個元素的equals()函數相等。
- 一般來說都需要重寫
equals()和hashCode()函數,尤其注意的是在重寫了hashCode()函數之后,該對象的HashCode值就和該對象的成員變量息息相關了(見后文),在進行任何與該對象的成員變量相關的函數操作的時候一定得注意HashCode的變化。雖然說成員量變量會改變,但是HashCode值卻不會跟着改變! - 如果都不相等,那就根據對象新計算好的hashCode值選擇存儲位置。這里也應該注意的是,根據HashCode實際上就是根據成員變量————因為HashCode就是根據成員變量算出來的!
class A {
@Override
public int hashCode() {
return 1;
}
}
class B {
@Override
public boolean equals(Object obj) {
return true;
}
}
class C {
@Override
public boolean equals(Object obj) {
return true;
}
@Override
public int hashCode() {
return 2;
}
}
public class MyTest {
public static void main(String[] args) {
HashSet books = new HashSet();
books.add(new A());
books.add(new A());
books.add(new B());
books.add(new B());
books.add(new C());
books.add(new C());
System.out.println(books);
}
}
運行上面的代碼並根據Set的不重復規則,我們可以想到如果集合即將添加的對象需要重寫equals()函數,但是hashCode()函數並沒有被重寫,會導致兩種結果:
要么不同hashCode的相同對象被存儲在不同位置上導致集合中出現相同的元素,要么相同hashCode的不同對象被鏈式存儲到同一個地方。
但是不管哪一種結果都是嚴重不符合Set定義的。
Note about overriding hashCode()
a.當兩個對象在比較equals()函數返回true的時候,HashCode()函數也應該返回相等的值。
重寫一個對象的equals()函數,通常情況下就是通過比較該對象中的所有實例變量。重寫HashCode()也一樣,只不過是使用一定的算法讓實例變量相等變成hashCode相等,我們很容易就想到了線性運算————直接加起來。下面給出基本數據類型的hashCode計算方法:
| 實例變量類型 | 計算方式 |
|---|---|
| boolean | hashCpde = (f?0:1); |
| 整數類型(byte, short, int , char) | hashCode = (int)f; |
| long | hashCode = (int)(f^(f>>>32)); |
| float | hashCode = Float.floatToBits(f); |
| double | long l = Double.doubleToLongBits(f); hashCode = f.hashCode(); |
| 引用類型 | hashCode = f.hashCode(); |
將每一個通過第一步計算出來的各個實例變量的值乘以一個質數相加(防止直接相加的冗余)得到該對象的新的HashCode值。
b.HashSet集合中同一個對象在程序運行過程中必須使用一個hashCode,一定要避免在過程中對該對象的一些實例變量進行修改。
主要是在程序運行的過程中,可能會有修改集合元素中的變量的可能,根據前面重寫算法和原理部分就可以知道:成員變量、HashCode、equals這三者基本等同起來了。
class A {
public int count;
public A(int count) {
this.count = count;
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if(obj != null && obj.getClass() == A.class) {
A a = (A)obj;
return this.count == a.count;
}
return false;
}
@Override
public int hashCode() {
return this.count;
}
@Override
public String toString() {
return "A[count] "+count + "]";
}
public class MyTest {
public static void main(String[] args) {
HashSet books = new HashSet();
books.add(new A(5));
books.add(new A(-3));
books.add(new A(9));
books.add(new A(-2));
System.out.println(books);
Iterator it = books.iterator();
A first = (A)it.next();
first.count = -3;
System.out.println(books);
books.remove(new A(-3));
System.out.println(books);
System.out.println(books.contains(new A(-3)));
System.out.println(books.contains(new A(5)));
}
}
(假設這里的第一個元素是5)看到上述代碼中,首先將第一個元素的count值改成了-3,這時候便會有兩個count = -3的元素存在,但是!
不要忘了,就算第一個元素的count被改成-3,它的HashCode是5!和最初加入HashSet的時候是一樣的!這一點可以通過輸出整個集合元素的HashCode的時候會發現。
所以當后面即將remove(-3)的時候,實際上集合首先會去找HashCode為-3的存儲位置————這時候只有最初count為-3的那個元素滿足條件,所以刪除的還是正確的,但是之后判斷集合中是否有-3?根據HashCode已經被刪掉了返回false;是否有5?雖然可以根據5找到那個被修改的元素,但是因為它的count值被修改為-3,所以equals也返回false。
LinkedHashSet
- 因為
HashSet是按照Hash算法來存儲元素的,所以元素的訪問順序往往和存儲順序不一致。有時候弧很不方便。LinkedHashset是HashSet的子類,它唯一的不同就是通過鏈表維護了元素的次序。 - 但是因為使用了鏈表,所以性能稍差一些。
