EnumSet詳細講解

https://blog.csdn.net/tugangkai/article/details/89631886css

以前介紹的Set接口的實現類HashSet/TreeSet,它們內部都是用對應的HashMap/TreeMap實現的,但EnumSet不是,它的實現與EnumMap沒有任何關係,而是用極爲精簡和高效的位向量實現的,位向量是計算機程序中解決問題的一種經常使用方式,咱們有必要理解和掌握。java

除了實現機制,EnumSet的用法也有一些不一樣。次外,EnumSet能夠說是處理枚舉類型數據的一把利器,在一些應用領域,它很是方便和高效。算法

下面,咱們先來看EnumSet的基本用法,而後經過一個場景來看EnumSet的應用,最後,咱們分析EnumSet的實現機制。數組

基本用法ruby

與TreeSet/HashSet不一樣,EnumSet是一個抽象類,不能直接經過new新建,也就是說,相似下面代碼是錯誤的:ui

 

EnumSet<Size> set = new EnumSet<Size>();

不過,EnumSet提供了若干靜態工廠方法,能夠建立EnumSet類型的對象,好比:this

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType)

noneOf方法會建立一個指定枚舉類型的EnumSet,不含任何元素。建立的EnumSet對象的實際類型是EnumSet的子類,待會咱們再分析其具體實現。spa

爲方便舉例,咱們定義一個表示星期幾的枚舉類Day,值從週一到週日,以下所示:.net

 

  1.  
    enum Day {
  2.  
    MONDAY, TUESDAY, WEDNESDAY,
  3.  
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
  4.  
    }

 能夠這麼用noneOf方法:3d

  1.  
    Set<Day> weekend = EnumSet.noneOf(Day.class);
  2.  
    weekend.add( Day.SATURDAY);
  3.  
    weekend.add( Day.SUNDAY);
  4.  
    System.out.println(weekend);

weekend表示休息日,noneOf返回的Set爲空,添加了週六和週日,因此輸出爲:

[SATURDAY, SUNDAY]

EnumSet還有不少其餘靜態工廠方法,以下所示(省略了修飾public static):

  1.  
    // 初始集合包括指定枚舉類型的全部枚舉值
  2.  
    <E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
  3.  
    // 初始集合包括枚舉值中指定範圍的元素
  4.  
    <E extends Enum<E>> EnumSet<E> range(E from, E to)
  5.  
    // 初始集合包括指定集合的補集
  6.  
    <E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
  7.  
    // 初始集合包括參數中的全部元素
  8.  
    <E extends Enum<E>> EnumSet<E> of(E e)
  9.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2)
  10.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
  11.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
  12.  
    <E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
  13.  
    <E extends Enum<E>> EnumSet<E> of(E first, E... rest)
  14.  
    // 初始集合包括參數容器中的全部元素
  15.  
    <E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
  16.  
    <E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

能夠看到,EnumSet有不少重載形式的of方法,最後一個接受的的是可變參數,其餘重載方法看上去是多餘的,之因此有其餘重載方法是由於可變參數的運行效率低一些。

應用場景

下面,咱們經過一個場景來看EnumSet的應用。

想象一個場景,在一些工做中,好比醫生、客服,不是每一個工做人員天天都在的,每一個人可工做的時間是不同的,好比張三多是週一和週三,李四多是週四和週六,給定每一個人可工做的時間,咱們可能有一些問題須要回答,好比:

  • 有沒有哪天一我的都不會來?
  • 有哪些天至少會有一我的來?
  • 有哪些天至少會有兩我的來?
  • 有哪些天全部人都會來,以便開會?
  • 哪些人週一和週二都會來? 

使用EnumSet,能夠方便高效地回答這些問題,怎麼作呢?咱們先來定義一個表示工做人員的類Worker,以下所示:

  1.  
    class Worker {
  2.  
    String name;
  3.  
    Set<Day> availableDays;
  4.  
     
  5.  
    public Worker(String name, Set<Day> availableDays) {
  6.  
    this.name = name;
  7.  
    this.availableDays = availableDays;
  8.  
    }
  9.  
     
  10.  
    public String getName() {
  11.  
    return name;
  12.  
    }
  13.  
     
  14.  
    public Set<Day> getAvailableDays() {
  15.  
    return availableDays;
  16.  
    }
  17.  
    }

爲演示方便,將全部工做人員的信息放到一個數組workers中,以下所示:

  1.  
    Worker[] workers = new Worker[]{
  2.  
    new Worker("張三", EnumSet.of(
  3.  
    Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.FRIDAY)),
  4.  
    new Worker("李四", EnumSet.of(
  5.  
    Day.TUESDAY, Day.THURSDAY, Day.SATURDAY)),
  6.  
    new Worker("王五", EnumSet.of(
  7.  
    Day.TUESDAY, Day.THURSDAY)),
  8.  
    };

每一個工做人員的可工做時間用一個EnumSet表示。有了這個信息,咱們就能夠回答以上的問題了。

哪些天一我的都不會來?代碼能夠爲:

  1.  
    Set<Day> days = EnumSet.allOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.removeAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

days初始化爲全部值,而後遍歷workers,從days中刪除可工做的全部時間,最終剩下的就是一我的都不會來的時間,這實際是在求worker時間並集的補集,輸出爲:

[SUNDAY]

有哪些天至少會有一我的來?就是求worker時間的並集,代碼能夠爲:

  1.  
    Set<Day> days = EnumSet.noneOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.addAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

輸出爲:

[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]

有哪些天全部人都會來?就是求worker時間的交集,代碼能夠爲:

  1.  
    Set<Day> days = EnumSet.allOf(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    days.retainAll(w.getAvailableDays());
  4.  
    }
  5.  
    System.out.println(days);

輸出爲:

[TUESDAY]

哪些人週一和週二都會來?使用containsAll方法,代碼能夠爲:

  1.  
    Set<Worker> availableWorkers = new HashSet<Worker>();
  2.  
    for(Worker w : workers){
  3.  
    if(w.getAvailableDays().containsAll(
  4.  
    EnumSet.of( Day.MONDAY,Day.TUESDAY))){
  5.  
    availableWorkers.add(w);
  6.  
    }
  7.  
    }
  8.  
    for(Worker w : availableWorkers){
  9.  
    System.out.println(w.getName());
  10.  
    }

輸出爲:

張三

哪些天至少會有兩我的來?咱們先使用EnumMap統計天天的人數,而後找出至少有兩我的的天,代碼能夠爲:

  1.  
    Map< Day, Integer> countMap = new EnumMap<>(Day.class);
  2.  
    for(Worker w : workers){
  3.  
    for(Day d : w.getAvailableDays()){
  4.  
    Integer count = countMap. get(d);
  5.  
    countMap.put(d, count== null?1:count+1);
  6.  
    }
  7.  
    }
  8.  
    Set<Day> days = EnumSet.noneOf(Day.class);
  9.  
    for(Map.Entry<Day, Integer> entry : countMap.entrySet()){
  10.  
    if(entry.getValue()>=2){
  11.  
    days.add(entry.getKey());
  12.  
    }
  13.  
    }
  14.  
    System.out.println(days);

輸出爲:

[TUESDAY, THURSDAY]

理解了EnumSet的使用,下面咱們來看它是怎麼實現的。

實現原理

位向量

EnumSet是使用位向量實現的,什麼是位向量呢?就是用一個位表示一個元素的狀態,用一組位表示一個集合的狀態,每一個位對應一個元素,而狀態只可能有兩種。

對於以前的枚舉類Day,它有7個枚舉值,一個Day的集合就能夠用一個字節byte表示,最高位不用,設爲0,最右邊的位對應順序最小的枚舉值,從右到左,每位對應一個枚舉值,1表示包含該元素,0表示不含該元素。

好比,表示包含Day.MONDAY,Day.TUESDAY,Day.WEDNESDAY,Day.FRIDAY的集合,位向量圖示結構以下:

對應的整數是23。

位向量能表示的元素個數與向量長度有關,一個byte類型能表示8個元素,一個long類型能表示64個元素,那EnumSet用的長度是多少呢?

EnumSet是一個抽象類,它沒有定義使用的向量長度,它有兩個子類,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一個long類型的變量做爲位向量,long類型的位長度是64,而JumboEnumSet使用一個long類型的數組。若是枚舉值個數小於等於64,則靜態工廠方法中建立的就是RegularEnumSet,大於64的話就是JumboEnumSet。

內部組成

理解了位向量的基本概念,咱們來看EnumSet的實現,同EnumMap同樣,它也有表示類型信息和全部枚舉值的實例變量,以下所示:

  1.  
    final Class<E> elementType;
  2.  
    final Enum[] universe;

elementType表示類型信息,universe表示枚舉類的全部枚舉值。

EnumSet自身沒有記錄元素個數的變量,也沒有位向量,它們是子類維護的。

對於RegularEnumSet,它用一個long類型表示位向量,代碼爲:

private long elements = 0L;

它沒有定義表示元素個數的變量,是實時計算出來的,計算的代碼是:

  1.  
    public int size() {
  2.  
    return Long.bitCount(elements);
  3.  
    }

對於JumboEnumSet,它用一個long數組表示,有單獨的size變量,代碼爲:

  1.  
    private long elements[];
  2.  
    private int size = 0;

靜態工廠方法

咱們來看EnumSet的靜態工廠方法noneOf,代碼爲:

  1.  
    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  2.  
    Enum[] universe = getUniverse(elementType);
  3.  
    if (universe == null)
  4.  
    throw new ClassCastException(elementType + " not an enum");
  5.  
     
  6.  
    if (universe.length <= 64)
  7.  
    return new RegularEnumSet<>(elementType, universe);
  8.  
    else
  9.  
    return new JumboEnumSet<>(elementType, universe);
  10.  
    }

getUniverse的代碼與上節介紹的EnumMap是同樣的,就不贅述了。若是元素個數不超過64,就建立RegularEnumSet,不然建立JumboEnumSet。

RegularEnumSet和JumboEnumSet的構造方法爲:

  1.  
    RegularEnumSet( Class<E>elementType, Enum[] universe) {
  2.  
    super(elementType, universe);
  3.  
    }
  4.  
    JumboEnumSet( Class<E>elementType, Enum[] universe) {
  5.  
    super(elementType, universe);
  6.  
    elements = new long[(universe.length + 63) >>> 6];
  7.  
    }

它們都調用了父類EnumSet的構造方法,其代碼爲:

  1.  
    EnumSet(Class<E>elementType, Enum[] universe) {
  2.  
    this.elementType = elementType;
  3.  
    this.universe = universe;
  4.  
    }

就是給實例變量賦值,JumboEnumSet根據元素個數分配足夠長度的long數組。

其餘工廠方法基本都是先調用noneOf構造一個空的集合,而後再調用添加方法,咱們來看添加方法。

添加元素

RegularEnumSet的add方法的代碼爲:

  1.  
    public boolean add(E e) {
  2.  
    typeCheck(e);
  3.  
     
  4.  
    long oldElements = elements;
  5.  
    elements |= ( 1L << ((Enum)e).ordinal());
  6.  
    return elements != oldElements;
  7.  
    }

主要代碼是按位或操做:

elements |= (1L << ((Enum)e).ordinal());

(1L << ((Enum)e).ordinal())將元素e對應的位設爲1,與現有的位向量elements相或,就表示添加e了。從集合論的觀點來看,這就是求集合的並集。

JumboEnumSet的add方法的代碼爲:

  1.  
    public boolean add(E e) {
  2.  
    typeCheck(e);
  3.  
     
  4.  
    int eOrdinal = e.ordinal();
  5.  
    int eWordNum = eOrdinal >>> 6;
  6.  
     
  7.  
    long oldElements = elements[eWordNum];
  8.  
    elements[eWordNum] |= ( 1L << eOrdinal);
  9.  
    boolean result = (elements[eWordNum] != oldElements);
  10.  
    if (result)
  11.  
    size++;
  12.  
    return result;
  13.  
    }

 

與RegularEnumSet的add方法的區別是,它先找對應的數組位置,eOrdinal >>> 6就是eOrdinal除以64,eWordNum就表示數組索引,有了索引以後,其餘操做與RegularEnumSet就相似了。

對於其餘操做,JumboEnumSet的思路是相似的,主要算法與RegularEnumSet同樣,主要是增長了尋找對應long位向量的操做,或者有一些循環處理,邏輯也都比較簡單,後文就只介紹RegularEnumSet的實現了。

RegularEnumSet的addAll方法的代碼爲:

  1.  
    public boolean addAll(Collection<? extends E> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.addAll(c);
  4.  
     
  5.  
    RegularEnumSet es = (RegularEnumSet)c;
  6.  
    if (es.elementType != elementType) {
  7.  
    if (es.isEmpty())
  8.  
    return false;
  9.  
    else
  10.  
    throw new ClassCastException(
  11.  
    es.elementType + " != " + elementType);
  12.  
    }
  13.  
     
  14.  
    long oldElements = elements;
  15.  
    elements |= es.elements;
  16.  
    return elements != oldElements;
  17.  
    }

類型正確的話,就是按位或操做。

刪除元素

remove方法的代碼爲:

  1.  
    public boolean remove(Object e) {
  2.  
    if (e == null)
  3.  
    return false;
  4.  
    Class eClass = e.getClass();
  5.  
    if (eClass != elementType && eClass.getSuperclass() != elementType)
  6.  
    return false;
  7.  
     
  8.  
    long oldElements = elements;
  9.  
    elements &= ~( 1L << ((Enum)e).ordinal());
  10.  
    return elements != oldElements;
  11.  
    }

主要代碼是:

elements &= ~(1L << ((Enum)e).ordinal());

~是取反,該代碼將元素e對應的位設爲了0,這樣就完成了刪除。

從集合論的觀點來看,remove就是求集合的差,A-B等價於A∩B’,B’表示B的補集。代碼中,elements至關於A,(1L << ((Enum)e).ordinal())至關於B,~(1L << ((Enum)e).ordinal())至關於B’,elements &= ~(1L << ((Enum)e).ordinal())就至關於A∩B’,即A-B。

查看是否包含某元素

contains方法的代碼爲:

  1.  
    public boolean contains(Object e) {
  2.  
    if (e == null)
  3.  
    return false;
  4.  
    Class eClass = e.getClass();
  5.  
    if (eClass != elementType && eClass.getSuperclass() != elementType)
  6.  
    return false;
  7.  
     
  8.  
    return (elements & (1L << ((Enum)e).ordinal())) != 0;
  9.  
    }

代碼也很簡單,按位與操做,不爲0,則表示包含。

查看是否包含集合中的全部元素

containsAll方法的代碼爲:

  1.  
    public boolean containsAll(Collection<?> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.containsAll(c);
  4.  
     
  5.  
    RegularEnumSet es = (RegularEnumSet)c;
  6.  
    if (es.elementType != elementType)
  7.  
    return es.isEmpty();
  8.  
     
  9.  
    return (es.elements & ~elements) == 0;
  10.  
    }

最後的位操做有點晦澀。咱們從集合論的角度解釋下,containsAll就是在檢查參數c表示的集合是否是當前集合的子集。通常而言,集合B是集合A的子集,即B⊆A,等價於A’∩B是空集∅,A’表示A的補集,以下圖所示:

 

上面代碼中,elements至關於A,es.elements至關於B,~elements至關於求A的補集,(es.elements & ~elements) == 0;就是在驗證A’∩B是否是空集,即B是否是A的子集。

只保留參數集合中有的元素

retainAll方法的代碼爲:

  1.  
    public boolean retainAll(Collection<?> c) {
  2.  
    if (!(c instanceof RegularEnumSet))
  3.  
    return super.retainAll(c);
  4.  
     
  5.  
    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
  6.  
    if (es.elementType != elementType) {
  7.  
    boolean changed = (elements != 0);
  8.  
    elements = 0;
  9.  
    return changed;
  10.  
    }
  11.  
     
  12.  
    long oldElements = elements;
  13.  
    elements &= es.elements;
  14.  
    return elements != oldElements;
  15.  
    }

從集合論的觀點來看,這就是求集合的交集,因此主要代碼就是按位與操做,容易理解。

求補集

EnumSet的靜態工廠方法complementOf是求補集,它調用的代碼是:

  1.  
    void complement() {
  2.  
    if (universe.length != 0) {
  3.  
    elements = ~elements;
  4.  
    elements &= - 1L >>> -universe.length; // Mask unused bits
  5.  
    }
  6.  
    }

這段代碼也有點晦澀,elements=~elements比較容易理解,就是按位取反,至關於就是取補集,但咱們知道elements是64位的,當前枚舉類可能沒有用那麼多位,取反後高位部分都變爲了1,須要將超出universe.length的部分設爲0。下面代碼就是在作這件事:

elements &= -1L >>> -universe.length; 

-1L是64位全1的二進制,咱們在剖析Integer一節介紹過移動位數是負數的狀況,上面代碼至關於:

elements &= -1L >>> (64-universe.length); 

若是universe.length爲7,則-1L>>>(64-7)就是二進制的1111111,與elements相與,就會將超出universe.length部分的右邊的57位都變爲0。

實現原理小結

以上就是EnumSet的基本實現原理,內部使用位向量,表示很簡潔,節省空間,大部分操做都是按位運算,效率極高。

小結

本節介紹了EnumSet的用法和實現原理,用法上,它是處理枚舉類型數據的一把利器,簡潔方便,實現原理上,它使用位向量,精簡高效。

對於只有兩種狀態,且須要進行集合運算的數據,使用位向量進行表示、位運算進行處理,是計算機程序中一種經常使用的思惟方式。

至此,關於具體的容器類,咱們就介紹完了。Java容器類中還有一些過期的容器類,以及一些不經常使用的類,咱們就不介紹了。

在介紹具體容器類的過程當中,咱們忽略了一個實現細節,那就是,全部容器類其實都不是從頭構建的,它們都繼承了一些抽象容器類。這些抽象類提供了容器接口的部分實現,方便了Java具體容器類的實現。若是咱們須要實現自定義的容器類,也應該考慮從這些抽象類繼承。

那,具體都有什麼抽象類?它們都提供了哪些基礎功能?如何進行擴展呢?讓咱們下節來探討。

相關文章
相關標籤/搜索