java基礎第18天



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 可變參數方法的要求
一個方法最多隻能有一個可變參數;
可變參數必須是最後一個參數;
可變參數只能出現在方法的形參中,局部變量或屬性是不能使用這種東西的。


















發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章