java.util.BitSet 类分析

一些概念:

一些逻辑上的位概念: 1,10 , 100 以上为 左移动<<

掩码 经常作为位预算中,通过与或者是 异或操作来获取所需要的值,最常见的如 子网掩码 255.255.255.0 通过最后一个8位的0x00来讲网内IP确定下来

使用long作为 一个单元

一个单元中的地址位数为 6 ,2的6次方为64 也就是所有的 移位 操作使用该数值表示64的计算操作

总计64 bit可以进行使用

同时所需的bit索引  掩码为 64-1 = 63

WORD_MASK 一个字的完整掩码 64位全为1的long

底层数据使用 long[] 的形式进行存储:bits 索引下标使用 i/64确定数组的位置,在通过i%64来确定bit的元素内位置

逻辑单元使用大小:unitsInUse

BitSet 构造函数:

无参数构造函数 默认使用 64位 作为初始化参数 调用有参数的构造函数

而有参数的 构造函数通过 /64的方法确定数组的长度(实际底层都是采用移位计算,向右移动6位)

为了避免计算余数的操作,都默认在原来计算出的数组长度上 +1 (能很好的避免,传入为0的情况)

unitIndex 单元下标计算:

输入 位的下标 ,通过/64的操作来确认

bit 获取单元内的掩码

通常的思路是i%64 获取位数t,然后 构造一个 只有一个t位为1的数值

1L<

也就是 获取 i的低六位的数值,注意这里的细节:63&i已经获取到了i%64的值,通过移位(1L<

具体单元内值的掩码

recalculateUnitsInUse 重新计算逻辑单元数

通过从大到小的方法,判断单元内的值 不为0 的情况

然后将以不为0的下标+1的方法重新计算unitsInUse 逻辑单元使用数

ensureCapacity 重新扩大底层单元数目

通过获取当前的大小*2 和传入的参数unitsRequired 所需单元中的最大值

作为新的底层数组的大小,然后重新实例化,再复制数据,同时废弃原来的数组

(这个函数,以及高度体现了,其类不可并发操作的原因,其复制和重新设定大小的时候,没有进行必要的锁定)

flip 针对具体某一位进行补码操作

获取单元下标,获取单元内掩码

判读unitsInUse 逻辑使用单元是否足够:

不足-》 调用ensureCapacity 扩大逻辑单元 ,使用掩码 进行异或 ,重新设定新的逻辑单元使用数

足-》 先使用掩码 进行异或 ,如果发现 逻辑使用单元下的数据单元最后一个单元为空(即为0) 则调用 recalculateUnitsInUse 重新计算逻辑单元个数

使用包括fromIndex 和 toIndex版本的 flip

同样的需要进行如下操作:

确保逻辑单元个数足够,根据toIndex计算得到 最后一个逻辑单元下标,来获取最大需要的逻辑单元数

判读 from 和to 是否在同一逻辑单元内:

如果是-》 获取两者的单元内位掩码 ,获取差值 之后 与 逻辑单元进行异或 , 同样在最后判读最后一个单元是否为空,(进行逻辑使用单元的优化)

如果不是-》面对跨单元的情况, 需要改变的地方有三个:

from端的 左边所有位(包括自己),

中间段 所有的单元

to端的 右边所有位

同时继续重新设定新的逻辑单元使用数

bitsRightOf 获取64位的 中指定位的所有右边为1的掩码

这里对于指定为是0的 直接返回 整个字的掩码 。不进行移位(优化)

bitsLeftOf 获取64位的 中指定为的所有左边为1的掩码

set 设定指定位 为true

非零判断

确定单元下标

判断逻辑使用单元是否足够,不够使用ensureCapacity 进行扩充

使用逻辑单元内的值 和 指定位的 掩码进行 或操作

对于另外一个可以设定值的版本 bitIndex value

如果是true调用 非参数的版本

如果是false调用 clear的实现

对于有from 和 to端的版本

采用类似于flip的from to的算法,只是将原来的异或运算改成了 或运算

clear 设定指定位 为false

与set类似,只是将 位计算改为 指定位的掩码取反然后相与计算方法

bits[unitIndex] &= ~bit(bitIndex);

针对from to端 的采用类似 flip上的结构,只是计算方法使用 取反相与的方法

clear 无参数版本

将所有底层的逻辑使用单元的值设定为0

get bitIndex 获取指定位的值

通过先找单元下标,再找单元内位掩码,通过掩码和当前单元的与运算的结果来决定

当前指定位的值, =0 返回 false , !=0 返回true

BitSet get(int fromIndex, int toIndex) 获取指定范围内的二进制集合

先进行了范围的判断包括:from>0 ,to>0 , from>to

如果长度 小于 from 或者 from == to 则直接返回一个空的

如果对于逻辑长度

通过to - from 获取需要实例化的 BitSet的大小

(to-from+63)/64 获取到需要复制的单元的个数

针对非最后一个单元的数据的复制:

假定from的 单元内偏移为x , 则偏移之后的剩余为64-x , to的单元内偏移为 y

复制的时候 假定需要复制的单元以此编号为 1,2,3

则不难看出复制的规则如下:

[1](64-x)+[2]x

[2](64-x)+[3]x

...

类推

在实现上 通过先将前一个单元的64-x位和第二个单元的x为进行高地位相与合并

实现上可以通过 单元数据的>>>x 第二个单元数据<

而针对最后一个需要复制的单元需要判断的条件为:

是否 (64-x单元内偏移) + y单元内偏移 <64 (程序中根据判断 基于位计算获取的单元数w 是否和 基于单元的单元数c是否相等,当 出现前面的条件时候 c=w+1)

如果是:直接获取 to右边所有的数据再通过向右移动from的位数的位数来获取最后的值(由于最后一个单元的x位已经作为上一个单元的复制中使用,将上面公式)

如果不是:则获取倒数第二个单元的64-X (M)和最后一个单元的 to以内的数据(N)进行合并,

相应的操作就是 M>>>x|N<

对于是否 (64-x单元内偏移) + y单元内偏移 <64 (程序中根据判断 基于位计算获取的单元数w 是否和 基于单元的单元数c是否相等,当 出现前面的条件时候 c=w+1) 的验证:

假定 x=60+64M y=20+64(M+L)

通过位计算的单元数 (y-x+63)/64 = (24+64L)/64 = L

通过基于单元位置计算的 x 为 M+1 y为M+L+1  两者之差为 L+1

由上可见当 y的单元内偏移量 小于 x的单元内偏移量 是才会出现

getBits (int j)

获取指定单元内的值

如果j

nextSetBit int(from)

从指定位置获取到 第一个为true的位位置

获取from所在的单元u,以及from所在的单元内偏移x(临时变量),将单元内的数据>>x 获取到实际需要测试的值 ,如果实际需要测试的值为0 设定x=0

接着是向后寻找 非0逻辑单元 ,同时修改u表示检索的当前单元

如果直到最后一个单元 还是 0则返回-1

通过调用trailingZeroCnt 确定非0单元内的,从低位到高位的连续0的个数n

最后返回 (64u+n+x)

注:这里+x是为了避免,由于在本单元内找到1,要加上单元内的因为移位操作而丢失的位数

trailingZeroCnt long val

从尾部寻找连续0的个数,直到找到1停止

核心思想:采用8位 ,8位的顺序从低到高的寻找, 通过获取 8位的值,通过查表方法获取 连续0的个数

具体的表是一个一元数组,包括256个数字

在实现上 JDK中采用了 恶心的, 8次的调用,(为什么不采用一个for 里面包一下)

具体的表可以查看源码,可以肯定的 对于奇数肯定是0

nextClearBit fromIndex

从from的位置找到第一个false

先确定from所在的单元 u

如果单元大于逻辑使用单元数 ,直接返回 from

对所在单元进行向右n移位获取实际要判断的位

获取需要判断的from 之后,也就是头 的位 判断是否存在 0 (这里将找0 分为 1.在同单元,2.在之后的单元,3.所有单元都没有)

如果存在 0(通过和字掩码的移位的值进行判断), 直接通过对 移位后的单元去反调用 trailingZeroCnt ,获取到 连续1的个数 x

那么最后的结果就是  64u+n+x

如果不存在 0 , 就需要向后找,直到找到一个包含0的单元(通过和字掩码进行比较) ,同时更新 临时变量 u,标记检索的单元下标

如果直到到了最后的逻辑单元还没有找到符合以上要求的单元,则返回 length() 也是就bitSet的长度

找到了一个符合的包含0的逻辑单元之后

这里进行了一个优化:如果查找到的u单元值为0 ,直接返回 64u (注意源码中还需要 + n,这里是为了包括在 和from 同一个单元的情况,而且刚好,from(包括自己)后面全是0的情况)

计算逻辑同上面的方法一致,单元值取反 trailingZeroCnt ,找到连续1的个数 x

最后的结果值 64u+x ,(在实现上,源码还加上了已经对非同一单元已经标记为0的n)

注意: 该函数中的 单元内右移动采用了,符号右移,也就说高位补1,这里之所以没有影响是因为我们要找的0 ,所以补1 是正确的,不过对于  WORD_MASK >> testIndex 字节掩码的符号右移,我觉得是个BUG,没有意义

length

获取集合的实际大小(根据最后一个逻辑单元内的最高设定为true的值的位置决定的)

首先判断逻辑使用单元是否为0 ,如果是0 ,直接返回 0

这里的计算方法为:

获取最后一个单元的数据,通过逻辑使用大小n的方法

通过先找高32位,再找低32位的方法

如果高位为0 直接 结果为 64(n-1)+ bitLen(低32位)(这里代码采用了强制转化,由于高32位为0,不会出现值溢出的可能)

如果高位不为0 结果为 32 + bitLen(高32位)

bitLen

获取 有效的位数(也就是从尾到头的找1的位置最大值)

这里主要用到了2分查找的方法:

深度5,平均5次值比较,最坏的情况为: 0 和 负数 由于两者 中如果是负数则最高位 是1 ,表示为 32 的长度 。而0 则为0

16

8

4

2

1

isEmpty 是否为空

通过和逻辑使用数 进行比较,如果为 0,表示为空

intersects

如果指定的 BitSet 中有设置为 true 的位,并且在此 BitSet 中也将其设置为 true,则返回 ture。

有就是从两个bitset的有效位开始,比较 所有位 相与 是否全为 0

如果全为0 表示 则返回false

cardinality 返回设定true的个数

通过遍历所有的逻辑使用单元,通过调用bitCount 获取单元内的true个数,最后对结果累加

bitCount(long val)

1010101....1 ,这里主要采用的是移位归并的方法, 核心思路就是将X区间内的1个数直接放到X的区间内

首先是 1 到 2 的归并 将所有的 相连两位中的1的个数 存到到 其两位的区间内

例如

1101 -》 1001 这是归并之后的结果 计算的方法是: 设定原来的位为abcd 将 0b0d 和 0a0c 进行计算 统计1 的个数

用数理知识很容易理解到

00 -》 00

01 -》 01

10 -》 01

11 -》 10

接着是 2 到 4 的归并 abcdefgh 将 00cd00gh 和 00ab00ef相加 得到的个数存到到 4位的字节中

以此类推

知道 32 到64 的归并

算法的思路是如此,具体的实现,可以有很多优化的细节,可以见网上的介绍 Hamming weight

and 将传入的集合 B 和当前集合 A 进行与运算

首先 判断集合A,B是否为同一个对象 如果是不计算

接着 获取A,B集合中最小的逻辑单元数 C

接着 对C范围内的单元进行 与运算

最后 大于A集合中不在C范围内的单元(大于C)的单元全部设定为 0

判断A集合中的最后单元是否为0 ,如果是则重新计算逻辑使用单元数

or 将传入的集合 B 和当前集合 A 进行或运算

首先 判断集合A,B是否为同一个对象 如果是不计算

接着 将当前A实际使用单元长度 >= 传入的集合B逻辑使用单元长度

接着 去两个集合中最小的 单元数 进行 或运算

最后 如果传入的集合大 将剩余的数据直接复制到当前集合内 ,

如果 B逻辑使用单元数 大于 A逻辑使用单元同时将 A的逻辑使用大小设定为B的逻辑使用大小

xor 异或计算 传入集合B ,本集合A

如果 A逻辑使用单元数大于B的逻辑使用单元数

采用B的逻辑使用单元数,对里面的树进行异或操作

如果 B的逻辑使用单元数大于A的逻辑使用单元数

设定A的逻辑大小和B一样,对原来A大小区域内的数据进行异或运算

最后将B中多出来的区域数据直接复制到A中

andNot 传入集合B

将 A中的位&=~B 来计算,对于超出的位 不进行计算,无论是 传入的还是集合中的

hashCode

10011010010

根据如下算法获得

public int hashCode() {
long h = 1234;
for (int i = words.length; --i >= 0; ) {
h ^= words[i] * (i + 1);
}
return (int)((h >> 32) ^ h);
}

size

集合中的实际位总数,通过存储单元长度*64来获取

equals

1.先比对类型

2.比较内存地址

3.比对其中的数据,

根据两者的最小有效单元数进行比较

再根据多出来的单元内是否都为0 ,来确定

clone 复制对象实例,以及其中的数据

readObject

覆盖了默认的序列化函数,用来对象反序列化时候的 重新计算 需要的逻辑使用单元数。

toString

计算出所有的位数,通过 逻辑使用单元数 unitsInUse * 64

通过get的方法 获取指定位中的值,

如果是true (1)

最后 调用 StringBuffer 将其位数下标加到 字符串中

注:这个函数 可以进行优化,通过实际遍历 有效单元内的值,而不是调用get的方法,或许是 这个函数不常用

写在后面

主要能学到一些基本的位操作的技巧 比如对于 mod (2的N次)的操作,可以通过直接进行掩码上的与操作

还有对于 2的倍数的 乘除可以通过移位进行优化 还有针对 >> 有符号右移动 >>> 无符号右移动 的区别针对负数的情况, 是采取高位补1的

这是基础的在内存中 对于负数的存储的知识  对其原值进行取反再加1 也就是 补码

还有 对于其中集合操作的如何对 单元long的操作上有很多的细节,

同时最后的经典的 Hamming Weight的算法针对计算设定1的个数上的 归并 树算法 也是不错的。通过查找网上的资料还是能够知道其算法是可以进行优化的,针对可溢出的计算,可以采用多项式和的方法对,最后8位的归并进行 乘法进行优化