Java集合(六) Set詳解

卡農Canon發表於2017-12-12

  前面我們學習了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元素。

Java集合(六) Set詳解
  從上面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。

相關文章