EnumSet源碼初探

EnumSet類定義

最近項目原因可能會用到EnumSet,於是週末利用閒暇時間閱讀了一下EnumSet的源碼,對源碼中的類說明和方法說明根據自己的理解進行了翻譯,水平有限,其中翻譯不對的地方希望熱心的朋友能夠批評指正。

EnumSet是一個抽象類,它有兩個子類:RegularEnumSet和JumboEnumSet,這兩個子類具體特性以後再做分析,這裏先學習一下EnumSet這個抽象類。

1、EnumSet是專門用於枚舉類型的一種Set實現,EnumSet裏所有的元素都來自同一個枚舉類,並且在Set創建的時候就需要顯式或隱式地指定這個枚舉類。EnumSet內部是使用位向量實現的,這種表示方法非常的緊湊和高效。這個類的空間和時間性能都很好,而且具有高質量、類型安全等優點,足以代替之前的基於int的位標識。對於EnumSet這種Collection而言,即使像containsAll和retainAll這種算法比較複雜的運算執行起來也會特別快。

JDK原文:
A specialized implementation for use with enum types. All of the elements in an enum set must come from a single enum type that is specified, explicitly or implicitly, when the set is created. Enum sets are represented internally as bit vectors. This representation is extremely compact and efficient. The space and time performance of this class should be good enough to allow its use as a high-quality, typesafe alternative to traditional int-based “bit flags.” Even bulk operations (such as containsAll and retainAll) should run very quickly if their argument is also an enum set.

2、EnumSet的iterator方法返回的迭代器(Iterator)遍歷順序是其自然順序(枚舉常量的聲明順序)。得到的迭代器是弱一致性的:永不會拋出ConcurrentModificationException異常,迭代過程中對Set的修改可能會對Set產生影響,也可能不會。

JDK原文:
The iterator returned by the iterator method traverses the elements in their natural order (the order in which the enum constants are declared). The returned iterator is weakly consistent: it will never throw {@link ConcurrentModificationException} and it may or may not show the effects of any modifications to the set that occur while the iteration is in progress.

3、EnumSet中不允許有Null元素,插入Null元素的時候會拋出
NullPointerException異常,然而測試Set中是否有Null元素或者嘗試從Set中移除一個Null元素是不會拋異常的。

JDK原文:
Null elements are not permitted. Attempts to insert a null element will throw NullPointerException. Attempts to test for the presence of a null element or to remove one will, however, function properly.

4、像其他大多數collection具體實現類一樣,EnumSet是非線程安全的。如果多個線程併發訪問同一個EnumSet,並且至少有一個線程對它進行修改,它就應該從外部進行同步。通常的做法是將封裝了這個EnumSet的對象進行同步。如果沒有這樣的對象,就應該通過Collections.synchronizeSet(set)這種方式將這個set同步。最好在創建的時候就調用上面的靜態方法將這個Set同步,以免發生非同步訪問。就像下面這樣:

Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));

JDK原文:
Like most collection implementations, EnumSet is not synchronized. If multiple threads access an enum set concurrently, and at least one of the threads modifies the set, it should be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the enum set. If no such object exists, the set should be “wrapped” using the Collections#synchronizedSet method. This is best done at creation time, to prevent accidental unsynchronized access:
Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));

5、實現注意:所有基本操作的時間複雜度都是O(1),這些操作執行起來比HashSet快得多(這一點並不保證)。當一些時間複雜度較高的方法的參數是EnumSet的時候,他們的實際時間複雜度也將是O(1)。

JDK原文:
Implementation note: All basic operations execute in constant time. They are likely (though not guaranteed) to be much faster than their HashSet counterparts. Even bulk operations execute in constant time if their argument is also an enum set.

原文中提到,EnumSet的存儲方式是“位標誌”,那麼何謂“位標誌”呢?我們都知道在Java中,一個byte的長度是8位,即00000000,假設每一位都能用來表示一盞燈的亮與滅,我們爲這8盞燈從右向左分別從1到8編號,那麼,
00000001表示1號燈亮,
00000010表示2號燈亮,
00000100表示3號燈亮,
……
將他們翻譯成10進制則就是:
1表示1號燈,
2表示2號燈,
4表示3號燈,
……
我們可以定義一個枚舉類:

public enum Lamp {
        lamp1((byte)1),  lamp2((byte)2), 
        lamp3((byte)4),  lamp4((byte)8),
        lamp5((byte)16), lamp6((byte)32), 
        lamp7((byte)64), lamp8((byte)128);

        private byte idx;

        private Lamp(byte idx) {
            this.idx = idx;
        }

        public byte idx() {
            return idx;
        }

    }

當需要一組來自這個枚舉中的幾個值的時候,比如需要將號碼爲奇數的燈放到一個集合的時候,我們先來看一下都需要幾號燈:1、3、5、7,對應的枚舉值是1、4、16、64,再來看一下他們的二進制表示:
00000001
00000100
00010000
01000000
可以發現在一列上1最多出現一次(因爲我們就是按照這個規律去編號的),若將這幾個數字按位或操作,會得到這樣一個二進制:
01010101
再結合剛剛的命名規則,能夠明顯地看出來這個“集合”中都包含哪些燈。
這樣我們僅僅使用一個short,就能表示擁有8個枚舉值的枚舉對象的集合,正如段落1中所言,佔用空間非常小。那麼爲何說它性能優越呢?我們看一下前面所提到的containsAll方法,爲了方便比較我們這裏結合RegularEnumSet子類進行分析。

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

        RegularEnumSet<?> es = (RegularEnumSet<?>)c;
        if (es.elementType != elementType)
            return es.isEmpty();

        return (es.elements & ~elements) == 0;
    }

上面是RegularEnumSet的containsAll方法的源碼,除了前面的驗證性的判斷之外,只需要關注最後的核心部分,那就是return的值:(es.elements & ~elements) == 0;
我們先分析一下其實現原理。
比如我們要判斷集合A是否包含所有的集合B的元素,就使用A.containsAll(B)進行判斷,那麼,elements就是A的所有元素,es.elements就是B的所有元素,return後的表達式可以簡化爲B & ~A == 0。
沿用上面“燈”的實例,假設A有1、3、5、7四盞燈,B有1、3兩盞燈,則A、B對應的二進制分別是:
A 和B —–> ~A和B
01010101 —-> 10101010
00000101 —-> 00000101
很明顯B & ~A 結果爲0。

其實從二進制來說,A包含B中所有的元素就是說B中所有是1的位置,A中也是1,將A取反,就會變成B中所有是1的位置,A中全是0,這時將B與~A進行與操作,結果必然是0。

原理上理解之後,我們發現只需要一個與操作,就能實現其他集合方法中時間複雜度非常高的containsAll操作,其執行時間也是常量級的,因此EnumSet的具有高效性。

接下來將會結合JDK源碼學習一下EnumSet的各個方法。

發佈了30 篇原創文章 · 獲贊 3 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章