Set接口概述
1 Set與Collection的關係
Set是一種Collection,即Set接口是Collection接口的子接口。
2 Set支持泛型
Java中所有集合類都支持泛型,這一點不在贅述。
3 Set沒有添加新方法
Set中的方法全部是從Collection繼承的,自己沒有添加任何一個方法。
4 Set的基本特性
模仿數學中的“集”。
不要求順序,
無重複元素
沒下標
5 Set的add()方法返回值有意義
List#add()方法返回值永遠是true
而Set#add()方法返回值可能是false,這一特性是因爲Set不包含重複元素。如果一個元素多次添加到同一Set中,那麼就會添加失敗,所以返回false。
6 遍歷Set只能使用Iterator
Set沒有下標,所以不能像List一樣使用下標來遍歷。
Set只能使用Iterator來遍歷了!
7 常用實現類
HashSet、TreeSet、LinkedHashSet
默認使用HashSet
HashSet
1 HashSet特性
不同步,即線程不安全的
無序
無重複元素(所有Set都是這個特性)
底層使用哈希表結構
2 使用String測試HashSet
測試toString()。
測試Iterator迭代,查看輸出順序。
測試add()、contains()、remove()方法
3 使用Person(自定義類型)測試HashSet
測試toString()。
測試Iterator迭代,查看輸出順序。
測試add()、contains()、remove()方法,注意這些方法是否能正確運行!
重寫Person類的equals()方法後,再測試add()、contains()、remove()等方法。
4 HashSet是怎麼保證元素唯一性的?
先比較hashCode()
再使用equals()比較
如果hashCode()相等,再使用equals()比較;如果hashCode()不相等,那麼就不再使用equals()比較了。
如果想讓你的哈希表聽話,需要重寫元素類型的兩個方法:
hashCode()和equals()
如果兩個對象的equals()比較爲true,那麼hashCode()必須相同。反之,沒有要求!
例如:Person比較的是兩個對象的name、age、sex,
那麼hashCode()使用name、age、sex的hashCode()相加,這就能保存上面的條件了。基本類型就本身也就可以了,要麼也可以轉換成對應的包裝器類型,再去獲取hashCode()。
哈希表結構
1 桶數組
哈希表是一個桶數組,也就是說一個哈希表中有多個桶!
每個桶可以存放多個元素。可以把桶理解爲鏈表(集合)!這樣整個哈希表就是擁有多個鏈表的集合了。
2 計算桶位
哈希表中有很多桶,即一個桶數組。所謂桶位就是對應桶數組的下標!
3 添加元素的流程
當把元素添加到哈希表中時,需要先找到元素對應的桶位,然後判斷這個桶中是否存在這個元素,如果元素在桶中已經存在,那麼添加失敗;否則添加成功!
獲取元素的哈希碼值(使用元素的hashCode()方法);
通過哈希碼值計算桶位(可以把哈希碼值理解爲就是桶位);
遍歷桶中元素,使用元素的equals()方法,驗證元素是否在桶中已經存在;
存在則添加失敗,否則把元素添加到桶中。
4 添加元素的問題
HashSet<Person> set = new HashSet<Person>();
Person p1 = new Person(“zhangSan”, 23, “male”);
Person p2 = new Person(“zhangSan”, 23, “male”);
set.add(p1);
set.add(p2);
上面代碼中set.add(p2)的結果會添加成功!也就是說HashSet會認識p1和p2是兩個不同對象。如果想讓上面代碼中set.add(p2)添加失敗,我們需要讓HashSet認爲p1和p2是相等的。
set.add(p2):調用p2的hashCode()方法獲取哈希碼,通過哈希碼找到桶。如果p1與p2的hashCode()不同,那麼p2找到的桶就與p1找到的桶不同。
循環遍歷p2對應桶中所有元素,使用equals()比較,如果沒有相同元素,那麼添加p2到這個桶中。
結論:就算p1.equals(p2)結果爲true,但p1.hashCode() != p2.hashCode(),那麼也是枉然!
5 HashSet保證元素唯一性
如果兩個對象的hashCode()相等,並且使用equals()方法比較返回true,那麼這兩個對象是相等的。
編寫equals()和hashCode()
1 euqals()方法
我們以Person類爲例:public boolean equals(Object o)
如果當前對象與o指向同一實例,那麼直接返回true
if(this == o) return true;
如果o不是Person類型,那麼直接返回false
boolean b = o instanceof Person;
if(!b) return false;
把o向下轉型爲Person類型
Person p = (Person)p;
如果當前對象的name與p對象的name不相等,那麼返回false
if(!name.equals(p.name)) return false;
如果當前對象的age與p對象的age不相等,那麼返回false
if(!age == p.age) return false;
如果當前對象的sex與p對象的sex不相等,那麼返回false
if(!sex.equals(p.sex)) return false;
執行到這裏,可以肯定兩個元素是相等的了,返回true
return true
public boolean equals(Object o) {
if(this == o) {
return true;
}
boolean b = o instanceof Person;
if(!b) {
return false;
}
Person p = (Person)o;
if(!name.equals(p.name)) {
return false;
}
if(age != p.age) {
return false;
}
if(!sex.equals(p.sex)) {
return false;
}
return true;
}
2 hashCode()方法
我們以Person類爲例:int hashCode()
定義int h = 0,最終返回h;
計算所有屬性的哈希值;
計算age:數值類型的屬性本身作爲值:
h += this.age;
計算name:引用類型的屬性調用其hashCode()方法:
h += name.hashCode();
計算sex:引用類型的屬性調用其hashCode()方法:
h += sex.hashCode();
最後返回h
對象的hashCode()都在100之內,這對計算桶的分配會過於其中。例如有一萬個桶,而元素都放到了0~10之間的桶中,其它桶中沒有元素,這就不好了!所以需要修改上面的hashCode()方法,放大多個對象哈希碼之間的間隔。
int h = 1;//如果爲0,那麼乘以基數也是枉然!
int prime = 31;//基數
h += prime * h + age;
h += prime * h + name.hashCode();
h += prime * h + sex.hashCode();
return h;
public int hashCode() {
int h = 1;
int prime = 31;
h += prime * h + age;
h += prime * h + name.hashCode();
h += prime * h + sex.hashCode();
return h;
}
LinkedHashSet
1 LinkedHashSet是HashSet的子類
LinkedHashSet是HashSet的子類,也就是說LinkedHashSet底層也使用的是哈希表!
2 LinkedHashSet迭代有序
LinkedHashSet的迭代順序是添加順序!因爲內部使用了鏈表來記錄元素的添加順序,迭代時再使用添加時順序迭代!
3 LinkedHashSet沒有添加新的方法
LinkedHashSet沒有添加新的方法,也就是說,它使用起來與使用HashSet一樣,只是它有可預知的順序,而HashSet是雜亂無章的。
TreeSet
TreeSet是有序的,它會把元素排序,但如果元素沒有自然順序,那就出錯。
要求元素類型,必須去實現Comparable接口。這個接口只有一個方法:
compareTo(T o),用來比較當前對象與參數對象誰大誰小。this > o,返回一個正數;this < 0,返回一個負數;否則返回0。
TreeSet用compareTo()方法保證元素的唯一性,如果新添加的元素,與當前Set中的元素使用compareTo比較結果爲0,那麼就說明元素已經存在,再添加就重複了,所以添加失敗!
1 TreeSet特性
不同步,即線程不安全。
有序
無重複元素(所有Set都是這個特性)
底層使用二叉樹結構
2 使用String元素測試TreeSet
測試迭代順序!
3 使用Person元素測試TreeSet
測試add()方法
4 TreeSet是怎麼保證元素唯一性的?
要求元素具有可比性!
例如:數值類型都具有可比性:10 > 3
例如:String類型具有可比性:”abc”.compareTo(“def”) < 0
但是,Person沒有可比性。
任何實現了Comparable接口的類都是具有可比性的!
當兩個元素使用compareTo()方法比較返回0時,表示兩個元素是相同的!
5 讓Person實現Comparable接口
實現Comparable接口只需要重寫一個方法:int compareTo(Person p)方法。
重寫這個方法需要指向出兩個Person對象如果比較,是比較姓名,還是比較年齡,或者所有屬性都參加比較。
public int compareTo(Person p) {
int n = name.compareTo(p.name);
if(n != 0) {
return n;
}
n = age - p.age;
if(n != 0) {
return n;
}
return sex.compareTo(p.sex);
}
上面代碼先是比較兩個對象的name,如果name可以分出大小,那麼就返回結果;
如果name分不出大小,那麼再去比較年齡,如果年齡分出大小,那麼就返回結果;
如果年齡再分出不結果,那麼最終以性別來比較。
6 不負責任的compareTo()方法會讓TreeSet出問題
因爲TreeSet是使用compareTo()方法來保證元素唯一性的,也就是說,在添加元素時,如果新添加的元素與TreeSet中任意一個元素使用compareTo()方法比較返回0時,那麼添加就會失敗!
例如,讓Person的compareTo()方法永遠返回0,這說明任何兩個元素比較的結果都是0,表示相等。這就會使TreeSet中最多有一個元素。
例如,讓Person的compareTo()方法使用年齡比較,而不使用name和sex,那麼只要年齡相等的元素就會被TreeSet視爲相等,那麼你會發現,在TreeSet中不可能存在兩個年齡相等的Person對象。
二叉樹
1 什麼是二叉樹
二叉樹也是一種樹狀結構。
二叉樹中每個節點最多有兩個子節點。
二叉樹中每個節點都比自己左邊節點的值大,比右邊節點的值小。
2 添加元素
添加進來的第一個元素就是根節點;
然後再添加第二個元素時,與根節點進行比較,如果比根節點大,那麼放到根節點的右側子節點位置;如果比根節點小,那麼放到根節點左側子節點位置;如果與根節點相等,那麼添加失敗。
添加第三個元素時,先與根比較:
如果比根小,那麼再與根的左側子節點進行比較:
大於左側節點,新元素放到左節點的右節點位置;
小於左節點,放到左節點的左節點位置;
等於左節點,那麼添加失敗。
如果比根大,那麼再與根的右側子節點進行比較:
大於右側節點,新元素放到右節點的右節點位置;
小於左節點,放到左節點的左節點位置;
等於左節點,那麼添加失敗。
如果等於根節點,那麼添加失敗。
例如:向TreeSet中添加Integer,分別是:2,5,8,3,6,9,3
第一步:添加2到樹中,因爲沒有元素,那麼2爲根節點:
第二步:添加5到樹中,因爲5大於根節點2,所以把5放到2的右邊:
第三步:添加8到樹中,因爲8大於根節點2,所以再讓8與5比較,因爲8大於5,所以把8放到5的右邊:
第四步:添加3到樹中,因爲3大於根節點2,所以再讓3與5比較,因爲3小於5,所以把3放到5的左邊:
第五步:添加6到樹中,因爲6大於根節點2,所以再讓6與5比較,因爲6大於6,所以讓6與8比較,因爲6小於8,所以把6放到8的左邊:
第六步:添加9到樹中,因爲9大於根節點2,所以再讓9與5比較,因爲9大於5,所以讓9與8比較,因爲9小於8,所以把9放到8的右邊:
第七步:添加3到樹中,因爲3大於根節點2,所以再讓3與5比較,因爲3小於5,所以讓3與3比較,因爲3與3相等,所以添加失敗!
3 遍歷二叉樹
遍歷二叉樹是原則是先左,然後當前節點,最後是右。
例如還是以上面二叉樹結構舉例說明:
2是根節點,那麼需要再遍歷2的左節點,但因爲2沒有左節點,所以打印;
然後是2的右節點了,即5;
因爲5有左節點,所以遍歷到3節點;
因爲3沒有左結節,所打印3,然後輪到3的右節點,但因爲3沒有右節點,所以打印3結節結束;
5的左節點打印結束,開始打印5本身,然後是打印5右側節點8;
因爲8有左節點,所先打印左節點6;
因爲6沒有左節點,所打印6本身,因爲6沒有右節點,所以打印6節點結束;
8節點的左側打印完畢,開始打印8本身,然後開始打印8的右側節點9;
因爲9沒有左節點,所以打印9本身,因爲9沒有右節點,所以打印9結束;
打印8結束;
打印5結束;
打印2結束。
比較器Comparator
如果TreeSet有比較器,那麼就使用比較器來比較元素;
如果TreeSet沒有比較器,那麼使用元素類型的自然順序;
如果元素類型沒有自然順序,那麼就出異常。
1 當TreeSet元素沒有實現Comparable接口
如果Person沒有實現Comparable接口,而且還想把Person對象添加到TreeSet中,這就需要使用給TreeSet添加比較器對象,即Comparable接口。
2 TreeSet排序的兩種選擇
要麼讓元素自己擁有可比較性,即自然順序;
要麼讓TreeSet本身擁有比較器。
TreeSet類的構造器:TreeSet(Comparator c)
3 Comparator接口
這個接口有兩個方法:
boolean equals(Object o):這個方法可以不去理睬它,因爲Object類也有這個方法;
int compare(T t1, T t2):比較參數1與參數2誰大誰小,t1>t2返回正數;t1<t2返回負數;否則返回0。
4 誰的優先級高
當元素擁有自然順序,並且TreeSet同時也擁有比較器,那麼最終使用什麼進行比較呢?答案是使用比較器比較。
也就是說,如果TreeSet擁有比較器,那麼使用比較器進行比較。
這說明修改比較邏輯不用去修改Person類的compareTo()方法,而只需給TreeSet一個新的比較器就可以了。
5 使用匿名內部類給TreeSet指定比較器
Arrays類
ArrayS類常用方法概述
本類所有方法都是靜態的!本類方法是針對數組的操作!
void sort(type[], int fromIndex, int toIndex)
int binarySearch(type[], int fromIndex, int toIndex, type key)
String toString()
2 sort()和binarySearch()方法與自然順序和比較器
當給引用類型排序,或者在引用類型數組中查找時,需要數組元素擁有自然順序,或者給方法指定比較器。
Collections
Collections常用方法概述
本類所有方法都是靜態的,本類方法都是針對集合的操作。
T max(Collection):求最大元素,依賴元素類型的自然順序;
T min(Collection):求最小元素;
void sort(Collection):給集合中元素排序,依賴元素類型的自然順序;
int binarySearch(List, T):在List中查找T,返回其下標。
void reverse(List):把List中所有元素位置反轉;
Comparator reverseOrder(Comparator):返回比較器;
Collection synchronizedCollection(Collection):把一個不同步的Collection轉換成同步;
List synchronizedList(List):把一個不同步的List轉換成一個同步的List;
Set synchronizedSet(Set):把一個不同步的Set轉換成一個同步的Set;
Map synchronizedMap(Map):把一個同步的Map轉換成一個同步的Map。
2 Collections和Collection的區別
Collectoins:類、所有方法都是靜態的,是集合的工具類,而不是集合類;
Collection:接口,是集合的根類,是List、Set的父接口。
增強for循環
1 增強for循環概念
可以循環遍歷數組或者集合類。
2 增強for循環的語法格式
for(元素類型 e : 數組或集合對象) {
}
增強for每循環一次,都會把數組或集合中的一個元素賦值給e,從頭開始遍歷,直到最後一個元素。
3 增強for的優缺點
只能從頭到尾的遍歷數組或集合,而不能只遍歷部分。
在遍歷List或數組時,不能獲取當前元素下標。
增強for使用便簡單。
增強for比使用迭代器方便一點!
4 增強for與Iterable接口
任何實現了Iterable接口的類,都可以使用增強for來遍歷。
靜態導入
1 什麼是靜態導入
靜態導入也需要使用import關鍵字;
靜態導入後,在調用靜態方法,以及使用靜態屬性時就可以不再給出類名了,例如向控制檯打印時可以把System.out.println()寫成out.println();System.exit(0)寫成exit(0)。
2 靜態導入的語法格式
import static 包名.類名.靜態方法名;
import static 包名.類名.靜態屬性名;
import static 包名.類名.*;
3 靜態導入真是雞肋啊
不建議使用!
使用靜態導入,使代碼可讀性降低!
可變參數
1 使用數組爲方法參數
int sum(int a, int b) {return a + b;}
int sum(int a, int b, int c) {return a + b;}
int sum(int a, int b, int c, int d) {return a + b + c + d;}
看上面代碼。我們知道這種重載是無止境的!
當函數的參數可以是0~n個時,我們最好的辦法就是使用數組來處理,例如把上面代碼修改爲一個函數:
int sum(int[] arr) {
int sum = 0;
for(int i = 0; i < arr.length; i++) {
sum +=arr[i];
}
return sum;
}
修改後的sum()方法可以計算0~N個整數的和,但調用sum()需要傳遞一個數組,這使調用這個函數很不方便。
int arr = {1,2,3,,4,5,5};
sum(arr);
2 可變參數方法的定義
可以把數組類型的參數定義爲可變參數,例如:
int sum(int[] arr) {
int sum = 0;
for(int i = 0; i < arr.length; i++) {
sum +=arr[i];
}
return sum;
}
int sum(int… arr) {
int sum = 0;
for(int i = 0; i < arr.length; i++) {
sum +=arr[i];
}
return sum;
}
上面代碼把int[] arr修改爲int… arr,其中arr就變成了可變參數。
可變參數其實就是數組。
3 調用可變參數方法
當調用int sum(int…arr)方法時就方便多了。如下方式的調用都是正確的:
int[] arr = {1,2,3}; sum(arr);,使用數組調用;
sum();,不給參數調用,表示傳遞0個元素的數組,即:int[] arr={}; sum(arr);
sum(5);,用一個參數調用,表示傳遞1個元素的數組,即:int[] arr={5}; sum(arr);
sum(2,3,4,5);,用多個參數調用,表示傳遞多個元素的數組,即:int[] arr={2,3,4,5}; sum(arr);。
調用可變參數方法,可以傳遞0~N個參數來調用,也可以直接傳遞數組來調用。
4 可變參數方法的要求
一個方法最多隻能有一個可變參數;
可變參數必須是最後一個參數;
可變參數只能出現在方法的形參中,局部變量或屬性是不能使用這種東西的。