内容主要转载自usafchn's Notes,然后在此基础上做了一些补充。
今天做项目的时候偶然用到EnumSet,EnumSet平时不太常用,比较陌生,于是点进去看了下源码,发现这个类还是比较有意思的,首先EnumSet是个抽象类,当我们调用EnumSet提供的静态函数创建对象的时候,实际创建的是RegularEnumSet或者JumboEnumSet,前者对应枚举成员少于64个的情况,后者不设枚举成员数量上限,当枚举成员数量大于64时,EnumSet实际创建的对象是JumboEnumSet类型。由于我定义的枚举成员数明显没到64,于是很自然的点进RegularEnumSet继续一探究竟…
如果你调用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位为有效位。
回顾一下前面提到的表达式:
1 |
-1L >>> -universe.length;
|
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数值有没有变化,这个方法在批量添加/删除的时候特别有用(不用一位一位判断了),以后可以借鉴哈。
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; } |
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[];