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<<(63&i)
也就是 获取 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情况,直接将 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)的方法
而针对最后一个需要复制的单元需要判断的条件为:
是否 (64-x单元内偏移) + y单元内偏移 <64 (程序中根据判断 基于位计算获取的单元数w 是否和 基于单元的单元数c是否相等,当 出现前面的条件时候 c=w+1)
如果是:直接获取 to右边所有的数据再通过向右移动from的位数的位数来获取最后的值(由于最后一个单元的x位已经作为上一个单元的复制中使用,将上面公式)
如果不是:则获取倒数第二个单元的64-X (M)和最后一个单元的 to以内的数据(N)进行合并,
相应的操作就是 M>>>x|N<<(64-x)
对于是否 (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<unitsInUse 逻辑使用单元 直接返回 单元内的值 , 要不然返回 0
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位的归并进行 乘法进行优化