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[];


你可能感兴趣的:(JDK源码解读之RegularEnumSet)