Set集合是無序的,元素不可重複的。
Set接口中的方法和Collection一致。
Set集合有三個重要子類HashSet,LinkedHashSet和TreeSet
1.HashSet
HashSet類實現 Set 接口,由哈希表(實際上是一個 HashMap 實例)支持。它不保證 set 的迭代順序;特別是它不保證該順序恆久不變(Set底層的存儲方式是由算法來完成的,所以說一定哪一天升級後算法就變化了,元素的存儲位置也就改變了)。此類允許使用 null 元素。 是不同步的。
什麼是哈希表?
散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。
簡單講,哈希函數是一種算法,通過這種算法算出來很多元素的地址值,把這些值存儲起來就叫做哈希表。
其實,哈希表裏面存儲的還是數組,如果一個數組想要查找元素,就要遍歷數組,一個一個去比較。而哈希這種算法對數組進行了優化。它將要存儲的元素代入到哈希函數中去,計算出一個位置,然後就把這個元素存儲到這個位置上,如果想要查找元素,就再算一遍位置,然後直接到這個位置上去獲取。
例如要存儲字符串“ab”到數組中去,一般情況下就直接把“ab”放到0角標上就行了。但是這種方法在查詢的時候需要遍歷數組逐個比較,速度慢。所以就根據元素自身的特點,定義一個函數,把“ab”代入到這個函數中去,對元素進行計算,獲取計算結果,這個結果就是“ab”在數組中的位置。這種方式的好處就是在查找“ab”在數組中的位置時,就不需要遍歷了,直接用“ab”再算一遍位置,然後去找這個位置就行了。這個算法就是哈希算法。
每個對象都有自己的哈希值,因爲每個對象都是Object類的子類,Object類中有方法int hashCode()返回該對象的哈希碼值,這個方法就是用來算對象哈希值的方法。這個方法是由Windows實現的,我們不用管,但是我們自己的對象可以覆蓋這個方法,建立對象自身的哈希值。
常用的哈希算法:
1. 直接尋址法:取關鍵字或關鍵字的某個線性函數值爲散列地址。即H(key)=key或H(key) = a•key + b,其中a和b爲常數(這種散列函數叫做自身函數)
2. 數字分析法
3. 平方取中法
4. 摺疊法
5. 隨機數法
6. 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key % p, p<=m。不僅可以對關鍵字直接取模,也可在摺疊、平方取中等運算之後取模。對p的選擇很重要,一般取素數或m,若p選的不好,容易產生同義詞。
如果哈希算法是字符Unicode碼求和再取餘
function(element){
//自定義哈希算法
97+98=195
returen 195%10;//結果是5
}
那麼“ab”的地址就是5,把“ab”放在數組角標爲5的位置上,查找的時候直接用“ab”再進行運算,算出結果是5,直接去角標5的位置上查找元素,然後判斷該元素是不是“ab”,如果不是,那麼這個數組中就沒有“ab”;如果已經把“ab”放在角標爲5的位置上,還要再存一個“ab”這個時候會先計算ab的位置,然後判斷是否相同,如果相同話就不再保存。所以這個算法提高了查詢效率,但是缺點就是不能保存重複數據。
怎麼判斷兩個元素的方式是否相同?判斷哈希值是否相同是使用hashCode方法,判斷內容是否相同是使用equals方法。
注意:如果hashCode不同,就不再判斷equals了,因爲肯定是兩個不同的元素。
如果想要存儲“ba”,首先計算哈希值,結果相同,然後判斷內容,結果不相同,這種情況叫做哈希衝突。
散列衝突的解決方案:
1. 開方定址法
這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突是,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p1爲基礎,產生另一個哈希地址p2……直到找出一個不衝突的哈希地址pi,將相應元素存入其中。
2. 再哈希法
這種方法是同時構造多個不同的哈希函數,當哈希地址衝突時,在用別的哈希函數進行計算,如果衝突再換哈希函數,直到算出一個不衝突的哈希地址。
3. 鏈地址法
這種方法的基本思想是將所有哈希地址爲i的元素構成一個成爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於經常進行插入和刪除的情況。
4.建立公共溢出區
將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一律填入溢出表。
存儲字符串對象
HashSet hs = new HashSet();
hs.add("ABC1");
hs.add("ABC2");
hs.add("ABC3");
hs.add("ABC4");
hs.add("ABC2");
hs.add("ABC3");
Iterator it = hs.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
輸出結果:
ABC1
ABC4
ABC2
ABC3
Set集合中存儲對象是無序的,並且相同的元素不會進行保存。
Set集合只有一種取出元素的方式,就是Iterator迭代器。
存儲自定義對象
在開發中,我們使用更多的是自定義對象而不是字符串對象
public class Person {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class HashSetTest{
public static void main(String[] args){
HashSet hs = new HashSet();
hs.add(new Person("lisi4",24));
hs.add(new Person("lisi7",27));
hs.add(new Person("lisi1",21));
hs.add(new Person("lisi9",29));
hs.add(new Person("lisi7",27));
Iterator it = hs.iterator();
while(it.hasNext()){
Person p = (Person)it.next();//自定義對象要做強轉動作,add接受後Person提升爲Object
System.out.println(p.getName()+"...."+p.getAge());
}
}
}
輸出結果:
lisi1….21
lisi7….27
lisi4….24
lisi7….27
lisi9….29
HashSet集合數據結構是哈希表,所以存儲元素的時候,使用元素的hashCode方法來確定位置,如果位置相同,再通過元素的equlas方法來判斷元素是否相同。
在該示例中,Person對象的哈希值是通過調用Object中的hashCode方法,並且判斷內容是否相同也是用的Object中的equals方法。這5個Person對象的hashCode不相同,所以HashSet集合認爲他們是5個不同的元素。所以我們需要建立自己的hashCode方法來計算元素的位置和equals方法來判斷元素是否相同,所以需要在Person類中複寫Object的hashCode和equals方法。
public class Person{
//定義變量,構造函數,get、set方法
public int hashCode(){
return name.hashCode+age();//人類的特點就是姓名和年齡,所以根據姓名和年齡計算地址值
}
public boolean equals(Object obj){
Person p = (Person)obj;
return this.name.equals(p.name) && this.age==p.age;//這裏調用的equals方法是字符串name中的equals方法,比較姓名是否相同
}
}
輸出結果:
lisi1….21
lisi9….29
lisi4….24
lisi7….27
在實際開發過程中一般要複寫hashCode、equals、toString方法,Eclipse中提供了快捷書寫hashCode和equals方法,toString方法的選項。
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());//爲了防止name.hashCode+age出現重複結果,就把age隨便乘以一個數
return result;
}
public boolean equals(Object obj) {
if (this == obj)
return true;
/*
if (!obj instanceof Person){
throw new ClassCastException("類型錯誤");如果傳入的類型不對,直接拋出類型錯誤
}
*/
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public String toString(){//如果直接使用從Object中繼承的toString方法,返回結果就是哈希值,沒有意義,一般情況下我們需要根據對象的實際情況,返回對對象的描述,所以需要重寫toString方法。
return name+":"+age;
}
2.TreeSet
TreeSet是按照元素的字典順序排序,這個順序我們不稱之爲有序,有序是指的存入和取出的順序。這裏的順序我們可以叫做指定的順序。
TreeSet是不同步的。
存儲字符串對象
比較簡單,略過。
存儲自定義對象
1. 自然排序(實現Comparable接口)
繼續使用Person類,創建TreeSet集合如下:
public class TreeSetDemo{
public static void main(String[] args){
TreeSet ts = new TreeSet();
ts.add(new Person("wangwu",21));
ts.add(new Person("zhaoliu",26));
ts.add(new Person("zhangsan",23));
ts.add(new Person("sunqi",27));
ts.add(new Person("lisi",21));
Iterator it = ts.iterator();
while (it.hasNext()) {
Person p = (Person)it.next();
System.out.println(System.out.println(p.getName()+"......"+p.getAge()););
}
}
}
運行發現報錯Person cannot be cast to java.lang.Comparable
因爲TreeSet是給元素排序用的,既然排序就要進行大小的比較,但是兩個Person對象不能進行比較。
Comparable接口
此接口強行對實現它的每個類的對象進行整體排序。這種排序被稱爲類的自然排序,類的 compareTo 方法被稱爲它的自然比較方法。
Person類用來描述人這類事務,如果想要把人放在TreeSet中進行排序,那麼Person應該在已經具備的基本功能外還要具備一個擴展功能,就是比較的功能。這個比較的功能已經被定義在了Comparable接口中,所以如果想要Person對象具備比較性,只要讓Person類實現Comparable接口就行了。
修改Person類
public class Person implements Comparable{
//hashCode,equals,toString,set,get,構造函數
public int compareTo(Object obj){
Person p = (Person)obj;
int temp = this.age - p.age;
return temp==0?this.name.compareTo(p.name):temp;//先按年齡排序,年齡相同再按姓名排序
//int temp = this.name.compareTo(p.name);先按姓名排序,姓名相同再按年齡排序
//return temp==0?this.age-p.age:temp;
}
}
輸出結果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27
在上面的例子中比較name時調用的compareTo方法是字符串中的compareTo方法,其實String類也實現了接口Comparable,字符串中的compareTo方法重寫了Comparable接口中的compareTo方法,所以字符串本身具備自然排序。只要對象想要進行比較,就要實現Comparable接口,並重寫compareTo方法。
這個Person類中默認的比較排序方式就是自然排序。
2. 比較器排序(實現Comarator接口)
但是如果我們不想要按照類中默認的方式排序,或者這個類沒有排序的功能怎麼辦?
首先不可以修改Person類,因爲這個類有可能不是我們寫的,我們只是拿過來用。所以我們可以使用TreeSet第二種排序方法,就是讓集合自身具備比較功能。
在第一種方法中,TreeSet集合自身並不能直接對元素進行比較,是元素自身具有比較的功能,TreeSet只是按照元素比較的結果來確定元素在集合中的位置。所以如果元素自身不具備比較的功能,可以讓集合對元素進行比較。
TreeSet構造方法:
TreeSet(Comparator< ? super E> comparator) :構造一個新的空 TreeSet,它根據指定比較器進行排序。插入到該 set 的所有元素都必須能夠由指定比較器進行相互比較:對於 set 中的任意兩個元素 e1 和 e2,執行 comparator.compare(e1, e2)
Comparator接口就是比較器。創建比較器就是實現Comparator接口,然後覆蓋其中的compare方法,把比較器作爲參數傳給TreeSet構造函數,TreeSet集合就具備了比較功能。如果自然排序和比較器同時存在的時候,以比較器爲主。
public class ComparatorByAge implements Comparator{
public int compare(Object o1,Object o2){
Person p1 = (Person)o1;
Person p2 = (Person)o2;
int temp = p1.getAge() - p2.getAge();
return temp==0?p1.getName().compareTo(p2.getName()):temp;
}
}
public class TreeSetDemo{
public static void main(String[] args){
TreeSet ts = new TreeSet(new ComparatorByAge);
//.....
}
}
輸出結果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27
在實際開發過程中常用的是比較器。但是一般情況下只要Person要存到集合中,除了覆蓋equals,hashCode,toString方法之外也還會實現Comparable接口。Java中的很多類都實現了Comparable接口,比如String類,Integer類,所以這些類都有比較的屬性,也就是說String類,Integer類本身具備自然排序。
TreeSet排序的底層原理(二叉樹)
如果TreeSet中的Person對象按照年齡來排序,首先第一個元素28放在樹的最頂層,然後第二個元素如果比28小就放在左邊,如果比28大就放在右邊…以此類推。當放25的時候,因爲25比28小,所以就放在28的左邊,不需要和29再進行比較,這樣就提高了效率。
但即使如此,當元素數量很多的時候速度也會變慢。因爲前面所有確定位置的元素都是按照元素從小到大的順序排列的,是有序的,爲了加快效率,就可以使用二分查找,在每一次放元素之前都會對已有的有序元素進行折半,再確定新元素的位置。
二叉樹判斷元素大小是看返回值的,如果返回1,就說明該元素比被比較的元素要大。如果返回-1,就說明該元素要小。
依據這原理,如果想要有序,按從小到大排列,就可以固定的返回1。
public class ComparatorByName implements Comparator {
public int compare(Object o1, Object o2) {
Person p1 = (Person)o1;
Person p2 = (Person)o2;
return 1;//有序。,返回-1就是倒敘
}
}
3.LinkedHashSet
具有可預知迭代順序的 Set 接口的哈希表和鏈接列表實現。此鏈接列表定義了迭代順序,即按照將元素插入到 set 中的順序(插入順序)進行迭代。
是鏈表結構的,存第一個元素,通過hashCode計算出地址,存第二個元素的時候,第一個元素記住第二個元素的地址…以此類推。
LinkedHashSet也是不同步的。
有了LinkedHashSet類,Set集合和List集合最大的不同點就是元素是否唯一了。