1. 程式人生 > >Java列舉型別(enum)-7

Java列舉型別(enum)-7

EnumSet原理

有前面位向量的分析,對於瞭解EnumSet的實現原理就相對簡單些了,EnumSet內部使用的位向量實現的,前面我們說過EnumSet是一個抽象類,事實上它存在兩個子類,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一個long型別的變數作為位向量,long型別的位長度是64,因此可以儲存64個列舉例項的標誌位,一般情況下是夠用的了,而JumboEnumSet使用一個long型別的陣列,當列舉個數超過64時,就會採用long陣列的方式儲存。先看看EnumSet內部的資料結構:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    //表示列舉型別
    final Class<E> elementType;
    //儲存該型別資訊所表示的所有可能的列舉例項
    final Enum<?>[] universe;
    //..........
}

EnumSet中有兩個變數,一個elementType用於表示列舉的型別資訊,universe是陣列型別,儲存該型別資訊所表示的所有可能的列舉例項,EnumSet是抽象類,因此具體的實現是由子類完成的,下面看看noneOf(Class<E> elementType)靜態構建方法

 public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        //根據EnumMap中的一樣,獲取所有可能的列舉例項
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            //列舉個數小於64,建立RegularEnumSet
            return new RegularEnumSet<>(elementType, universe);
        else
            //否則建立JumboEnumSet
            return new JumboEnumSet<>(elementType, universe);
    }

從原始碼可以看出如果列舉值個數小於等於64,則靜態工廠方法中建立的就是RegularEnumSet,否則大於64的話就建立JumboEnumSet。無論是RegularEnumSet還是JumboEnumSet,其建構函式內部都間接呼叫了EnumSet的建構函式,因此最終的elementType和universe都傳遞給了父類EnumSet的內部變數。如下:

//RegularEnumSet構造
RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
  }

//JumboEnumSet構造
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
      elements = new long[(universe.length + 63) >>> 6];
  }

在RegularEnumSet類和JumboEnumSet類中都存在一個elements變數,用於記錄位向量的操作,

//RegularEnumSet
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 3411599620347842686L;
    //通過long型別的elements記錄位向量的操作
    private long elements = 0L;
    //.......
}

//對於JumboEnumSet則是:
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 334349849919042784L;
    //通過long陣列型別的elements記錄位向量
    private long elements[];
     //表示集合大小
    private int size = 0;

    //.............
    }

在RegularEnumSet中elements是一個long型別的變數,共有64個bit位,因此可以記錄64個列舉常量,當列舉常量的數量超過64個時,將使用JumboEnumSet,elements在該類中是一個long型的陣列,每個陣列元素都可以儲存64個列舉常量,這個過程其實與前面位向量的分析是同樣的道理,只不過前面使用的是32位的int型別,這裡使用的是64位的long型別罷了。接著我們看看EnumSet是如何新增資料的,RegularEnumSet中的add實現如下

public boolean add(E e) {
    //檢測是否為列舉型別
    typeCheck(e);
    //記錄舊elements
    long oldElements = elements;
    //執行位向量操作,是不是很熟悉?
    //陣列版:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements |= (1L << ((Enum)e).ordinal());
    return elements != oldElements;
}

關於elements |= (1L << ((Enum)e).ordinal());這句跟我們前面分析位向量操作是相同的原理,只不過前面分析的是陣列型別實現,這裡用的long型別單一變數實現,((Enum)e).ordinal()通過該語句獲取要新增的列舉例項的序號,然後通過1左移再與 long型別的elements進行或操作,就可以把對應位置上的bit設定為1了,也就代表該列舉例項存在。圖示演示過程如下,注意universe陣列在EnumSet建立時就初始化並填充了所有可能的列舉例項,而elements值的第n個bit位1時代表列舉存在,而獲取的則是從universe陣列中的第n個元素值。

這就是列舉例項的新增過程和獲取原理。而對於JumboEnumSet的add實現則是如下:

public boolean add(E e) {
    typeCheck(e);
    //計算ordinal值
    int eOrdinal = e.ordinal();
    int eWordNum = eOrdinal >>> 6;

    long oldElements = elements[eWordNum];
    //與前面分析的位向量相同:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements[eWordNum] |= (1L << eOrdinal);
    boolean result = (elements[eWordNum] != oldElements);
    if (result)
        size++;
    return result;
}

關於JumboEnumSet的add實現與RegularEnumSet區別是一個是long陣列型別,一個long變數,運算原理相同,陣列的位向量運算與前面分析的是相同的,這裡不再分析。接著看看如何刪除元素

//RegularEnumSet類實現
public boolean remove(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    long oldElements = elements;
    //將int型變數j的第k個位元位設定為0,即j= j&~(1<<k)
    //陣列型別:a[i>>SHIFT] &= ~(1<<(i &MASK));

    elements &= ~(1L << ((Enum)e).ordinal());//long遍歷型別操作
    return elements != oldElements;
}


//JumboEnumSet類的remove實現
public boolean remove(Object e) {
        if (e == null)
            return false;
        Class<?> eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        int eOrdinal = ((Enum<?>)e).ordinal();
        int eWordNum = eOrdinal >>> 6;

        long oldElements = elements[eWordNum];
        //與a[i>>SHIFT] &= ~(1<<(i &MASK));相同
        elements[eWordNum] &= ~(1L << eOrdinal);
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size--;
        return result;
    }

刪除remove的實現,跟位向量的清空操作是同樣的實現原理,如下: 

至於JumboEnumSet的實現原理也是類似的,這裡不再重複。下面為了簡潔起見,我們以RegularEnumSet類的實現作為原始碼分析,畢竟JumboEnumSet的內部實現原理可以說跟前面分析過的位向量幾乎一樣。o~,看看如何判斷是否包含某個元素

public boolean contains(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;
    //先左移再按&操作
    return (elements & (1L << ((Enum)e).ordinal())) != 0;
}

public boolean containsAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet))
        return super.containsAll(c);

    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
    if (es.elementType != elementType)
        return es.isEmpty();
    //~elements取反相當於elements補集,再與es.elements進行&操作,如果為0,
    //就說明elements補集與es.elements沒有交集,也就是es.elements是elements的子集
    return (es.elements & ~elements) == 0;
}

對於contains(Object e) 方法,先左移再按位與操作,不為0,則表示包含該元素,跟位向量的get操作實現原理類似,這個比較簡單。對於containsAll(Collection<?> c)則可能比較難懂,這裡分析一下,elements變數(long型別)標記EnumSet集合中已存在元素的bit位,如果bit位為1則說明存在列舉例項,為0則不存在,現在執行~elements 操作後 則說明~elements是elements的補集,那麼只要傳遞進來的es.elements與補集~elements 執行&操作為0,那麼就可以證明es.elements與補集~elements 沒有交集的可能,也就是說es.elements只能是elements的子集,這樣也就可以判斷出當前EnumSet集合中包含傳遞進來的集合c了,藉著下圖協助理解:

圖中,elements代表A,es.elements代表S,~elements就是求A的補集,(es.elements & ~elements) == 0就是在驗證A’∩B是不是空集,即S是否為A的子集。接著看retainAll方法,求兩個集合交集

public boolean retainAll(Collection<?> c) {
        if (!(c instanceof RegularEnumSet))
            return super.retainAll(c);

        RegularEnumSet<?> es = (RegularEnumSet<?>)c;
        if (es.elementType != elementType) {
            boolean changed = (elements != 0);
            elements = 0;
            return changed;
        }

        long oldElements = elements;
        //執行與操作,求交集,比較簡單
        elements &= es.elements;
        return elements != oldElements;
    }

最後來看看迭代器是如何取值的

 public Iterator<E> iterator() {
        return new EnumSetIterator<>();
    }

    private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
        //記錄elements
        long unseen;

        //記錄最後一個返回值
        long lastReturned = 0;

        EnumSetIterator() {
            unseen = elements;
        }

        public boolean hasNext() {
            return unseen != 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (unseen == 0)
                throw new NoSuchElementException();
            //取值過程,先與本身負執行&操作得出的就是二進位制低位開始的第一個1的數值大小
            lastReturned = unseen & -unseen;
            //取值後減去已取得lastReturned
            unseen -= lastReturned;
            //返回在指定 long 值的二進位制補碼錶示形式中最低位(最右邊)的 1 位之後的零位的數量
            return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
        }

        public void remove() {
            if (lastReturned == 0)
                throw new IllegalStateException();
            elements &= ~lastReturned;
            lastReturned = 0;
        }
    }

比較晦澀的應該是

//取值過程,先與本身負執行&操作得出的就是二進位制低位開始的第一個1的數值大小
lastReturned = unseen & -unseen; 
//取值後減去已取得lastReturned
unseen -= lastReturned;
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];

我們通過原理圖來協助理解,現在假設集合中已儲存所有可能的列舉例項變數,我們需要把它們遍歷展示出來,下面的第一個列舉元素的獲取過程,顯然通過unseen & -unseen;操作,我們可以獲取到二進位制低位開始的第一個1的數值,該計算的結果是要麼全部都是0,要麼就只有一個1,然後賦值給lastReturned,通過Long.numberOfTrailingZeros(lastReturned)獲取到該bit為1在64位的long型別中的位置,即從低位算起的第幾個bit,如圖,該bit的位置恰好是低位的第1個bit位置,也就指明瞭universe陣列的第一個元素就是要獲取的列舉變數。執行unseen -= lastReturned;後繼續進行第2個元素的遍歷,依次類推遍歷出所有值,這就是EnumSet的取值過程,真正儲存列舉變數的是universe陣列,而通過long型別變數的bit位的0或1表示儲存該列舉變數在universe陣列的那個位置,這樣做的好處是任何操作都是執行long型別變數的bit位操作,這樣執行效率將特別高,畢竟是二進位制直接執行,只有最終獲取值時才會操作到陣列universe。

ok~,到這關於EnumSet的實現原理主要部分我們就分析完了,其內部使用位向量,儲存結構很簡潔,節省空間,大部分操作都是按位運算,直接操作二進位制資料,因此效率極高。當然通過前面的分析,我們也掌握位向量的運算原理。好~,關於java列舉,我們暫時聊到這。