写在前面
未经允许不得转载!
java.util.BitSet 这个类其实很简单,但是我觉得很有意思,所以专门写篇文章说下怎么个有意思法!
进入正题
首先从名字上大致就知道这个类是干嘛的,存储 Bit 的 Set,Bit 就是二进制数据01(理解成标志位就行,0没有,1有),而Set就是数学中的那个Set(集合),自己查资料。
拓展点:BitMap,有兴趣自己查资料。
先说下我是怎么理解 BitSet 设计理念的:
先开辟一块连续的内存(long 数组),因为long占64个bit,所以数组的每个格子就是64个bit。
private long[] words;
找到对应的数组的格子,即找到数组的下标。要设置0到63号标志位的,定位到数组的下标是0,64到127定位到数组的下标是1,依此类推,简单说找下标就是位置x/64。
int wordIndex = wordIndex(bitIndex);
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD; // ADDRESS_BITS_PER_WORD = 6
}
计算出并拿到指定编号位置设置标志位的值。上面说的数组的下标找到了,但是怎么设置一个bit位呢,long不是占64个bit吗?这里开始就有点意思了,比如说我要设置56号的标志位为1,那么我只要在long的64位中的第56号位设置为1就行了,这时需要用到移位操作,理论上有两种移动方式(1L<<55和Long.MIN_VALUE>>55),BitSet的设计者使用的是1向左移。为什么我说有意思呢?想想上一步找到数组的下标,比如2在数组的下标是0,56在数组的下标是0,65在数组的下标是1。同时long的左移操作是周期性的(具体查看 jvm 规范的 lshl 指令的描述及Notes,看完再看后面那句你就懂了),简单说就是:x & 0x3f,因为63有效位的二进制全是1,所以等价于 x%64。
(1L << bitIndex)
我不知道到读者有没有想明白这两步,简单说下吧,后面还会涉及到,如果没搞明白后面可能会懵逼:
假设有一块连续的内存,按 bit 存,上面的所有数据都是0,然后我们把他按照64个bit一个进行等分,此时懂 long 数组的意思了吧!
然后以数组的第0个下标(一个 long 值,64个bit位)为坐标轴原点,这时就知道每个数组下标对应的bit位的范围了,比如第0个下标就是0-63,第一个就是64-127,依此类推。
然后以64个bit位的第0个bit位为坐标轴原点,这时就能精确定位到指定的bit位了。
首次设置bit标志位。将上一步拿到的指定编号位置设置标志位的值直接赋值到数组的指定位置的下标就行了。
words[wordIndex] = (1L << bitIndex);
同个数组下标位置非首次设置bit标志位。上面说的只是数组的某个格子第一次设置值,如果有第二次设置的话还是这么操作的话就会被覆盖了,这时就需要按位或操作了,比如说第1次设置56号的标志为1,计算出并拿到指定编号位置设置标志位的值是(1L<<55),然后根据首次设置bit标志位的操作,将其赋值给数组的0下标;然后第2此设置21号的标志为1,计算出并拿到指定编号位置设置标志位的值是(1L<<20) ,这时就不能直接给赋值给数组的0下标了,而是要进行按位或((1L<<20) | (1L<<55))第21、56号都为1了,然后赋值给数组的0下标。
words[wordIndex] |= (1L << bitIndex);
查询指定指定bit位是否为1。比如说21号,首先先找到数组的下标(看前面说,找到的下标为0),计算出并拿到指定编号位置设置标志位的值(还是看前面说,值为(1L<<20)),然后取出数组下标对应的值(比如说 arr[0]=0),然后进行按位与(0 & (1L<<20)),最后得到的结果为0,所以为false。
int wordIndex = wordIndex(bitIndex);
((words[wordIndex] & (1L << bitIndex)) != 0)
最后再来说下我觉得最有意思的 toString 方法,其它方法读者自行研究。迟点补上。。。。。。
可以从低位开始,也可以从高位开始查询,BitSet 的实现是从低位开始查询,也就是数组的0下标值的最右边bit位开始查询。
有意思的来了:
拿到首个标志位为1的值。比如说数组0下标位置存的值是((1<<20)|(1<<55)),然后看下这个值最右边(因为是左移)bit位为0的个数(20个),这时就拿到最低位的bit位编号是21了。
拓展:用java.lang.Long#numberOfTrailingZeros 来计算右边bit位有多少个0的方式很有意思有兴趣自己看,如果是我来实现:
int zeroCount = 0;
long bNum = (1L << 20) | (1L << 55);
for (int i = 0; i < 64; i++) {
long curBit = (bNum >> i) & 1L;
if (curBit == 1) {
break;
}
zeroCount++;
}
拿到同个数组下标剩下标志位为1的值。根据上一步已经指定第一个值为1bit位编号了,此时获取下一位时就需要过滤掉前面的bit位了,直接用BitSet的代码:
(WORD_MASK << fromIndex); // WORD_MASK = 0xffffffffffffffffL; 也就是bit位全是1值
比如说我已经找到第21号了,那么fromIndex就是21,看我的解法代码:
int zeroCount = 0;
long bNum = (1L << 20) | (1L << 55);
int fromIndex = 0;
for (int i = fromIndex; i < 64; i++) {
long curBit = (bNum >> i) & 1L;
if (curBit == 1) {
break;
}
zeroCount++;
}
System.out.println(zeroCount);
// 找到第二个
bNum &= (0xffffffffffffffffL << (zeroCount + 1));
fromIndex = zeroCount;
for (int i = fromIndex; i < 64; i++) {
long curBit = (bNum >> i) & 1L;
if (curBit == 1) {
break;
}
zeroCount++;
}
System.out.println(zeroCount);
做下优化就能遍历查询当前下标的所有的bit为1的编号了,然后找数组的下一个下标重复此操作。
BitSet 有很多巧妙的解法,可以以我的解法为跳板去理解。
BitSet 找到第一个bit位为1的编号:
int i = nextSetBit(0);
public int nextSetBit(int fromIndex) {
int u = wordIndex(fromIndex);
long word = words[u] & (WORD_MASK << fromIndex);
while (true) {
if (word != 0)
return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);
if (++u == wordsInUse)
return -1;
word = words[u];
}
}
BitSet 找到第其余bit位为1的编号:
int i = nextSetBit(0); // 找到的是第一个bit位为1的编号
if (i != -1) {
while (true) {
if ((i = nextSetBit(i)) < 0) break;
int endOfRun = nextClearBit(i);
while (++i != endOfRun);
}
}
public int nextClearBit(int fromIndex) {
int u = wordIndex(fromIndex);
if (u >= wordsInUse)
return fromIndex;
// 这种解法太巧妙了,直呼牛逼!!!
// 取反后0变1,1变0,也就是以前设置了bit为1的位置现在会变成0
// 然后按位与上 fromIndex编号后全是0,fromIndex编号前全是1的数
// 最终得到就是fromIndex编号后才有1的一个值,再想想取反了,所以有1的就是原先是0,0就是原先的1
// 然后 Long.numberOfTrailingZeros(word) 一下就拿到了下一个bit位为1的编号
long word = ~words[u] & (WORD_MASK << fromIndex);
while (true) {
if (word != 0)
return (u * BITS_PER_WORD) + Long.numberOfTrailingZeros(word);
if (++u == wordsInUse)
return wordsInUse * BITS_PER_WORD;
word = ~words[u];
}
}
还有更多的方法自己去挖掘吧,包括扩容什么的。
拓展一下:
有了这篇文章的基础,再看java.util.JumboEnumSet
这个类就和喝水一样简单了。