內容主要轉載自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[];