主要谈谈BitSet的不足,然后重点说明开源Lucene的SparseFixedBitSet是如何解决的。
BitSet的优点在于节省存储空间,比如有10000个正整数范围从0-9999,底层使用byte数组方式存储大约占用1250个字节,如果使用整形数组存储大约占用4*10000=40000个字节,如此节省了大约32倍的空间。
如果有100个正整数,但数值范围在0-100000000之间,其中最大值为99999999,这个时候用BitSet存储将占用12500000个字节,而用整形数组存储大约占用100*4=400个字节,因此BitSet存储稀疏数据的时候反而是浪费空间的,解决的方法是使用稀疏BitSet,以Lucene的SparseFixedBitSet为例:
(1)首先将存储空间按照4096进行划分,即数值范围0-4095的为一组、4096-8191的为一组等等。
SparseFixedBitSet中开辟了二维数组long[][]进行存储,数组的第一个下标表示组ID,比如1000的组ID=1000/4096=0。
(2)数组的第二个下标表示数值所在的数据块ID,比如在原始的bitset中(底层用long数组)1000的数据块ID=1000/64=15。这里的意思是一样的,但表示的方法不同,也是最最关键的地方!很明显1000在数组的第二个下标中的位置一定不是15,不然费了这么大劲等于没优化,SparseFixedBitSet另外开劈了一个索引是用一维数组long[]表示的,数组下标是数值的组ID与二维数组的第一个下标一样的意思,long值记录的是1000的数据块ID=15(注意是按位进行存储的indexValue |=1<<15,由于1个long值可以表示64个数据块,每个数据块的long可以存储64个数值正好64*64=4096,这就是为什么存储空间按照4096划分的原因。),由于只有1个数据块,所以数据块ID被变换成0,如果来了一个新数值800,它的数据块ID=800/64=12即indexValue |=1<<12,此时有两个数据块了,原来数据块ID=15的被变换成1,数据块ID=12的被变换成0,再来一个新数据600的时候indexValue |=1<<9,原来数据块ID=15的被变换成2,数据块ID=12的被变换成1,数据块ID=9的被变换成0。
(3)将上面的话用代码总结一下:
long[] index;
long[][] bits;
1000->index[0] |= 1<<15 ; bits[0][0] |=1<<1000;
800 -> index[0] |=1<<12 ; bits[0][0] |=1<<800; bits[0][1] |=1<<1000;
600 -> index[0] |=1<<9 ; bits[0][0] |=1<<600; bits[0][1] |=1<<800; bits[0][2] |=1<<1000;
因此数据块ID=index中当前位后面1的个数:1000时index=00000000_00000000_00000000_00000000_00000000_00000000_01000000_00000000,所以1000的数据块ID=0;
800时index=00000000_00000000_00000000_00000000_00000000_00000000_01001000_00000000,所以1000的数据块ID=1,800的数据块ID=0;
600时index=00000000_00000000_00000000_00000000_00000000_00000000_01001001_00000000,所以1000的数据块ID=2,800的数据块ID=1;600的数据块ID=0;