前面我們學習了List集合。我們知道List集合代表一個元素有序、可重複的集合,集合中每個元素都有對應的順序索引。今天我們要學習的是一個注重獨一無二性質的集合:Set集合。我們可以根據原始碼上的簡介對它進行初步的認識:
/*
* A collection that contains no duplicate elements. More formally, sets
* contain no pair of elements <code>e1</code> and <code>e2</code> such that
* <code>e1.equals(e2)</code>, and at most one null element. As implied by
* its name, this interface models the mathematical <i>set</i> abstraction.
*/
複製程式碼
這一段說明了Set這個介面的作用,是一個不包含重複元素的集合。這裡的重複指,如果元素e1.equals(e2)是true,就不能包含兩個。而且最多也只包含一個null元素。
從上面Set的類結構圖可以看出,Set介面並沒有對Collection做任何擴充套件。物件的相等性
引用到堆上同一個物件的兩個引用是相等的。如果對兩個引用呼叫hashCode方法,會得到同樣的結果,如果物件所屬的類沒有覆蓋Object的hashCode方法的話,hashCode會返回每個物件特有的序號(Java是依據物件的記憶體地址計算出來此序號),所以兩個不同的物件的hashCode是不可能相等的。
如果想要讓兩個不同的Person物件視為相等的,就必須重寫從Object繼承下來的hashCode方法和equals方法,因為Object的hashCode方法返回的是該物件的記憶體地址,所以必須重寫,才能保證兩個不同的物件具有相同的hashCode,同時也需要兩個不同物件比較equals方法會返回true。
Set集合
特點
- Set集合中的元素是唯一的,不可重複(取決於hashCode和equals方法),也就是說具有唯一性。
- Set集合中元素不保證存取順序,並不存在索引。
繼承關係
Collection
|--Set:元素唯一,不保證存取順序,只可以用迭代器獲取元素。
|--HashSet:雜湊表結構,執行緒不安全,查詢速度較快。元素唯一性取決於hashCode和equals方法。
|--LinkedHashSet:帶有雙向連結串列的雜湊表結構,執行緒不安全,保持存取順序,保持了查詢速度較快特點。
|--TreeSet:平衡排序二叉樹(紅黑樹)結構,執行緒不安全,按自然排序或比較器存入元素以保證元素有序。元素唯一性取決於ComparaTo方法或Comparator比較器。
|--EnumSet:專為列舉型別設計的集合,因此集合元素必須是列舉型別,否則會丟擲異常。有序,其順序就是Enum類內元素定義的順序。存取的速度非常快,批量操作的速度也很快。
HashSet
原始碼對於HashSet的介紹簡潔明瞭:這個類實現了Set介面,由雜湊表支援(實際上是一個HashMap例項)。它不保證集合的迭代順序;特別是它不能保證隨著時間的推移,順序保持不變。這個類允許使用null元素。這個類是執行緒不安全的。
所以說看看常用的原始碼註釋還是非常有必要的。
HashSet的equals和hashCode
雜湊表裡存放的是雜湊值。HashSet儲存元素的順序並不是按照存入時的順序,是按照雜湊值來存的,所以取資料也是按照雜湊值取的。
元素的雜湊值是通過元素的hashCode方法來獲取的,HashSet首先判斷兩個元素的雜湊值,如果雜湊值一樣,接著會比較equals方法,如果equals結果為true,HashSet就視為同一個元素,只儲存一個(重複元素無法放入)。如果equals為false就不是同一元素。
基於HashMap實現
HashSet儲存的物件都被作為HashMap的key值儲存到了HashMap中。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
複製程式碼
我們知道HashMap是不允許有重複的key值(至於為什麼,大家可以先查詢資料),所以,這也保證了HashSet儲存的唯一性。
LinkedHashSet
照個舊,先看一下原始碼對LinkedHashSet的定義:由雜湊表和連結串列實現,可以預知迭代順序。這個實現與HashSet的不同之處在於,LinkedHashSet維護著一個執行於所有條目的雙向連結串列。這個連結串列定義了迭代順序,按照元素的插入順序進行迭代。
可以理解為:HashSet集合具有的優點LinkedHashSet集合都具有。而且LinkedHashSet集合在HashSet查詢速度快的前提下,能夠保持元素存取順序。
LinkedHashSet特徵總結
LinkedHashSet是HashSet的一個子類,LinkedHashSet也根據HashCode的值來決定元素的儲存位置,但同時它還用一個連結串列來維護元素的插入順序,插入的時候既要計算hashCode還要維護連結串列,而遍歷的時候只需要按照連結串列來訪問元素。
通過LinkedHashSet的原始碼可以知道,LinkedHashSet沒有定義任何方法,只有四個構造方法。再看父類,可以知道LinkedHashSet本質上也是基於LinkedHashMap實現的。LinkedHashSet所有方法都繼承於HashSet,而它能維持元素的插入順序的性質則是繼承於LinkedHashSet。
TreeSet
來繼續看TreeSet的定義:基於TreeMap實現的NavigableSet。根據元素的自然順序進行排序,或根據建立Set時提供的Comparator進行排序,具體取決於使用的構造方法。
TreeSet實現了SortedSet介面(NavigableSet介面繼承了SortedSet介面),顧名思義這是一種排序的Set集合,根據原始碼可以知道底層使用TreeMap實現的,本質上是一個紅黑樹原理。也正因為它排了序,所以相對HashSet來說,TreeSet提供了一些額外的根據排序位置訪問元素的方法。例如:first(),last(),lower(),higher(),subSet(),headSet(),tailSet()。
TreeSet的排序分兩種型別,一種是自然排序;一種是定製排序;
自然排序
TreeSet會呼叫compareTo方法比較元素大小,然後按升序排序。所以自然排序中的元素物件,都必須實現了Comparable介面。不然就會丟擲異常。對於TreeSet判斷元素是否重複的標準,也是呼叫元素從Comparable介面繼承的compareTo方法,如果返回0就是重複元素(返回一個 -1,0,或1表示這個物件小於、等於或大於指定物件。)。其實Java常見的類基本已經實現了Comparable介面。舉個例子吧:
public class Person implements Comparable {
public String name;
public int age;
public String gender;
public Person() {
}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String toString() {
return "Person [name=" + name + ", age=" + age + ", gender=" + gender
+ "]\r\n";
}
@Override
public int compareTo(@NonNull Object o) {
Person p = (Person) o;
if (this.age > p.age) {
return 1;
}
if (this.age < p.age) {
return -1;
}
return this.name.compareTo(p.name);
}
}
複製程式碼
這邊我們先建立一個Person類,實現Comparable介面,重寫了compareTo方法。排序條件是,先按照年齡進行排序,年齡相同的情況下,再比較姓名。我們再測試一下:
public class TreeSetTest {
public static void main(String args[]) {
TreeSet ts = new TreeSet();
ts.add(new Person("A", 24, "男"));
ts.add(new Person("B", 23, "女"));
ts.add(new Person("C", 18, "男"));
ts.add(new Person("D", 18, "女"));
ts.add(new Person("D", 20, "女"));
ts.add(new Person("D", 20, "女"));
System.out.println(ts);
System.out.println(ts.size());
}
}
複製程式碼
結果如下:
[Person [name=C, age=18, gender=男]
, Person [name=D, age=18, gender=女]
, Person [name=D, age=20, gender=女]
, Person [name=B, age=23, gender=女]
, Person [name=A, age=24, gender=男]
]
5
複製程式碼
非常直觀的可以看出,排序是先根據年齡再根據姓名排序的。而且根據元素個數和結果,知道TreeSet去了重。
定製排序
TreeSet另外一種排序就是定製排序,也叫自定義比較器。這種一般是在元素本身不具備比較性,或者元素本身具備的比較性不滿足要求,這個時候就只能讓容器自身具備。定製排序,需要關聯一個Comparator物件,由Comparator提供邏輯。
一般步驟為,定義一個類實現Comparator介面,重寫compare方法。然後將該介面的子類物件作為引數傳遞給TreeSet的構造方法。舉個例子:
public class TreeSetTest {
public static void main(String args[]) {
TreeSet ts = new TreeSet(new MyComparator());
ts.add(new Person("A", 24, "男"));
ts.add(new Person("B", 23, "女"));
ts.add(new Person("C", 18, "男"));
ts.add(new Person("D", 18, "女"));
ts.add(new Person("D", 20, "女"));
ts.add(new Person("D", 20, "女"));
System.out.println(ts);
System.out.println(ts.size());
}
class MyComparator implements Comparator {
public int compare(Object o1, Object o2) {
Person p1 = (Person) o1;
Person p2 = (Person) o2;
if (p1.age < p2.age) {
return 1;
}
if (p1.age > p2.age) {
return -1;
}
return p1.name.compareTo(p2.name);
}
}
}
複製程式碼
這次排序規則是年齡先按照從大到小(倒序),然後再根據姓名的自然排序進行元素的總體排序。Person類沒變,依然實現Comparable介面,在兩種排序都有的情況下,我們覺得結果會是怎樣的呢?
[Person [name=A, age=24, gender=男]
, Person [name=B, age=23, gender=女]
, Person [name=D, age=20, gender=女]
, Person [name=C, age=18, gender=男]
, Person [name=D, age=18, gender=女]
]
5
複製程式碼
可以看出,當Comparable比較方式,及Comparator比較方式同時存在,以Comparator比較方式為主。其他的都沒有疑問。
異同
Comparable是由物件自己實現的,一旦一個物件封裝好了,compare的邏輯就確定了,如果我們需要對同一個物件增加一個欄位的排序就比較麻煩,需要修改物件本身。好處是對外部不可見,呼叫者不需要知道排序的邏輯,只要呼叫排序就可以。
而Comparator由外部實現,比較靈活,對於需要增加篩選條件,只要新增一個Comparator即可。缺點是所有排序邏輯對外部暴露,需要物件外部實現。(這裡的外部指物件的外部,我們可以封裝好所有的Comparator,對呼叫者隱藏內部邏輯。)優點是非常靈活,隨時可以增加排序方法,只要物件內部欄位支援,類似動態繫結。
EnumSet
EnumSet顧名思義就是專為列舉型別設計的集合,因此集合元素必須是列舉型別,否則會丟擲異常。EnumSet集合也是有序的,其順序就是Enum類內元素定義的順序。EnumSet存取的速度非常快,批量操作的速度也很快。EnumSet主要提供以下方法,allOf, complementOf, copyOf, noneOf, of, range等。注意到EnumSet並沒有提供任何建構函式,要建立一個EnumSet集合物件,只需要呼叫allOf等方法。
EnumSet用的非常少,元素效能是所有Set元素中效能最好的,但是它只能儲存Enum型別的元素。
總個結吧
主要介紹了Set的結構,實現原理。Set只是Map的一個馬甲,主要邏輯都交給Map實現。東西不多,我們在後面Map的學習中對實現原理再深入研究。再提一嘴:
- 看到array,就要想到角標。
- 看到link,就要想到first,last。
- 看到hash,就要想到hashCode,equals。
- 看到tree,就要想到兩個介面。Comparable,Comparator。