JDK源碼解讀之RegularEnumSet

內容主要轉載自usafchn's Notes,然後在此基礎上做了一些補充。

緣由

今天做項目的時候偶然用到EnumSet,EnumSet平時不太常用,比較陌生,於是點進去看了下源碼,發現這個類還是比較有意思的,首先EnumSet是個抽象類,當我們調用EnumSet提供的靜態函數創建對象的時候,實際創建的是RegularEnumSet或者JumboEnumSet,前者對應枚舉成員少於64個的情況,後者不設枚舉成員數量上限,當枚舉成員數量大於64時,EnumSet實際創建的對象是JumboEnumSet類型。由於我定義的枚舉成員數明顯沒到64,於是很自然的點進RegularEnumSet繼續一探究竟…

addAll()函數

如果你調用EnumSet的靜態函數allOf()函數,那麼實際將會調用到的是RegularEnumSet中的addAll()函數,addAll()函數的實現只有一行:

1
2
if (universe.length != 0)
    elements = -1L >>> -universe.length;

真正引起我興趣的也正是這行代碼,先解釋一下幾個變量的含義:elements是long類型的64爲整數,用來存儲枚舉值;universe是一個數組,裏面存放了全部枚舉類型,length是數組長度,一個正數,前面加了負號,表示要移的位數是小於0的數。

移位運算

衆所周知,Java的移位運算符有三個:<<、>>和>>>,第一個是左移,後兩個分別是帶符號右移和無符號右移,那麼移位運算符的右邊竟然是一個負數,到底什麼意思呢?百度一下無果,於是想到了Oracle官方的JAVA語言規範[^java],翻了一下,好傢伙,官方文檔果然對移位運算規定的清清楚楚,其描述是這樣的:

If the promoted type of the left-hand operand is int, only the five lowest-order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & with the mask value 0x1f (0b11111). The shift distance actually used is therefore always in the range 0 to 31, inclusive.

If the promoted type of the left-hand operand is long, then only the six lowest- order bits of the right-hand operand are used as the shift distance. It is as if the right-hand operand were subjected to a bitwise logical AND operator & with the mask value 0x3f (0b111111). The shift distance actually used is therefore always in the range 0 to 63, inclusive.

看到了吧,大意就是移位操作符左邊如果是int類型,則操作符右邊的數只有低5位有效(右邊的數會首先與0x1f做AND運算),如果操作符左邊是long類型,右邊的數就只取低6位爲有效位。

再看addAll()函數

回顧一下前面提到的表達式:

1
-1L >>> -universe.length;

左邊是一個long類型,-1L的補碼錶示是0xffffffffffffffff,並且>>>是無符號右移,在右移的時候最高位補0;右邊的”-universe.length”其實只有低6位有效,舉個例子來說吧,假設length爲5,那麼-5在內存中的表示爲0xfffffffb,取低6位有效,那麼實際有效值是0x3b,換成十進制就是59,也就是把-1L右移59位,可見,表達式的結果正好是低5位全1,高位全0。

更一般地,當n處在[1..64]之間時,(-1L >>> -n)的結果應該是低n位全1,高位全0。可見,這個結果正好滿足RegularEnumSet的需要。

### 聯想

知道了這個trick以後,其實它還有更多用途,比如可以這樣:

1
2
3
4
5
6
7
8
// 代碼節選自java.lang.Long類
public static long rotateLeft(long i, int distance) {
    return (i << distance) | (i >>> -distance);
}

public static long rotateRight(long i, int distance) {
    return (i >>> distance) | (i << -distance);
}

是不是很有意思呢?


添加/刪除操作

知道移位操作的含義後,再看RegularEnumSet中其它成員函數就非常簡單了(本來這個類就沒什麼技術含量,不是麼?),比較有意思的是這個類判斷元素是否添加/刪除成功的方法,比如在add()函數中,它是這麼實現的:

1
2
3
long oldElements = elements;
elements |= (1L << ((Enum)e).ordinal());
return elements != oldElements;

同樣,remove()函數中,它是這麼實現的:

1
2
3
long oldElements = elements;
elements &= ~(1L << ((Enum)e).ordinal());
return elements != oldElements;

這個類裏面判斷元素有沒有添加/刪除成功,它沒有事先去判斷對應比特位上的數是0還是1,而是看添加/刪除後elements數值有沒有變化,這個方法在批量添加/刪除的時候特別有用(不用一位一位判斷了),以後可以借鑑哈。

size()函數

RegularEnumSet中還有一個比較有意思的成員函數是size()函數,size()函數是求Set中包含幾個元素,也就是求長整數elements二進制表示中1個個數。

求一個二進制數中1的個數方法太多,有沒有較爲高效的方法呢?先來看一下JDK是怎麼實現的吧:

1
2
3
4
5
6
7
8
9
public static int bitCount(long i) {
	i = i - ((i >>> 1) & 0x5555555555555555L);
	i = (i & 0x3333333333333333L) + ((i >>> 2) & 0x3333333333333333L);
	i = (i + (i >>> 4)) & 0x0f0f0f0f0f0f0f0fL;
	i = i + (i >>> 8);
	i = i + (i >>> 16);
	i = i + (i >>> 32);
	return (int)i & 0x7f;
}

這個方法技巧性很強,初次看很不容易看懂,基本思想是把二進制中相鄰位相加,然後以2位爲單位再合併,再4位合併……直到把所有位都合併了。上面的代碼確實非常難懂,不過可以換種寫法,性能略微低點,但是好理解啊:

1
2
3
4
5
6
7
8
9
public static int bitCount(int n) 
{ 
    n = (n &0x55555555) + ((n >>1) &0x55555555) ; 
    n = (n &0x33333333) + ((n >>2) &0x33333333) ; 
    n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ; 
    n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ; 
    n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ; 
    return n ; 
}

是不是清楚了很多呢,本人還是比較喜歡這種寫法,代碼形式也更加對稱,可讀性還強。

 詳細請參閱:《The Java® Language Specification —— Java SE 8 Edition》

===================================================================================================================

上面這篇文章中介紹的方法不多,重點在於移位操作符,個人感覺理解以爲操作符是理解EnumSet的關鍵。

EnumSetIterator的next()方法

這個方法我覺得也很巧妙(大概也是個人水平問題吧,位操作這種用的相當少。)。

public E next() {
            if (unseen == 0)
                throw new NoSuchElementException();
            lastReturned = unseen & -unseen; //unseen & -unseen返回的是unseen的二進制字符串最右邊第一個非0位代表的十進制數,自己寫寫比較一下結果就出來了。
            unseen -= lastReturned;
            return (E) universe[Long.numberOfTrailingZeros(lastReturned)]; //Long.numberOfTrailingZeros()返回的是lastReturned的二進制字符串的最右邊連續多少位爲0。如果最右邊爲二進制位爲1則結果爲0.</span>

        }


EnumSet在構造的時候如果length < 64則返回的是RegularEnumSet,否則返回的是JumboEnumSet。JumboEnumSet和RegularEnumSet基本一致,只不過在保存值的時候使用的是一個long型數組變量,而RegularEnumSet只用一個long型變量保存,JumboEnumSet在做一些操作的時候需要先定位到是數組中那個元素,然後所要做的操作和RegularEnumSet基本上是一樣的

/**
     * Bit vector representation of this set.  The ith bit of the jth
     * element of this array represents the  presence of universe[64*j +i]
     * in this set.
     */
    private long elements[];


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章