Java集合(六) Set詳解

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

  從上面Set的類結構圖能夠看出,Set接口並無對Collection作任何擴展。

對象的相等性

  引用到堆上同一個對象的兩個引用是相等的。若是對兩個引用調用hashCode方法,會獲得一樣的結果,若是對象所屬的類沒有覆蓋Object的hashCode方法的話,hashCode會返回每一個對象特有的序號(Java是依據對象的內存地址計算出來此序號),因此兩個不一樣的對象的hashCode是不可能相等的。
  若是想要讓兩個不一樣的Person對象視爲相等的,就必須重寫從Object繼承下來的hashCode方法和equals方法,由於Object的hashCode方法返回的是該對象的內存地址,因此必須重寫,才能保證兩個不一樣的對象具備相同的hashCode,同時也須要兩個不一樣對象比較equals方法會返回true。ide

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存儲的惟一性。ui

LinkedHashSet

  照個舊,先看一下源碼對LinkedHashSet的定義:由哈希表和鏈表實現,能夠預知迭代順序。這個實現與HashSet的不一樣之處在於,LinkedHashSet維護着一個運行於全部條目的雙向鏈表。這個鏈表定義了迭代順序,按照元素的插入順序進行迭代。
  能夠理解爲:HashSet集合具備的優勢LinkedHashSet集合都具備。並且LinkedHashSet集合在HashSet查詢速度快的前提下,可以保持元素存取順序。this

LinkedHashSet特徵總結

  LinkedHashSet是HashSet的一個子類,LinkedHashSet也根據HashCode的值來決定元素的存儲位置,但同時它還用一個鏈表來維護元素的插入順序,插入的時候既要計算hashCode還要維護鏈表,而遍歷的時候只須要按照鏈表來訪問元素。
  經過LinkedHashSet的源碼能夠知道,LinkedHashSet沒有定義任何方法,只有四個構造方法。再看父類,能夠知道LinkedHashSet本質上也是基於LinkedHashMap實現的。LinkedHashSet全部方法都繼承於HashSet,而它能維持元素的插入順序的性質則是繼承於LinkedHashSet。spa

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。
相關文章
相關標籤/搜索