学习了Java BitSet之后,写下这篇博文记录一下BetSet的原理及使用场景。
一、BitSet原理
BitSet,通过这个名字字面意思(位集合),就可以知道,它的使用应该与位(Bit)有关。事实也确实如此,它(BitSet)用于标志一个“东西”是否存在于这个“东西”的一个集合里面。这句话很拗口,举实例来说明,比如:一个字符是否存在一个字符串中。一个数字是否存在一个数字集合中等等,也就是可以用BitSet来进行大量数据的统计和筛选。
那么,BitSet是如何标志的呢?考虑一下这样的一个方案,假如有由20个不同的数字组成的数组,需要确定这20个数字分别是否等于某一个数字A。那么可以定义一个长度是20的boolean数组,然后遍历数字数组,判断数组元素是不是等于这个数字A,如果不等于,则将boolean数组对应下标的元素设置为false,如果等于,则将对应下标的元素设置成true。这是在数组(集合)元素少的情况,假如,我需要确定的是在一千万或者一亿个数字分别是否等于数字A呢?
这个时候如果你使用数组的话就会有一个内存销量的问题,因为boolean变量不是true就是false,也就是不是1就是0,在内存中只占用1位就可以了,但是java并没有明确指出boolean类型占用内存的大小,有可能是1位,有可能是1个字节,有可能是4个字节。那么如果不是1位的情况,那么这么大数量的boolean数组,占用的内存就很大了,造成了浪费。
BitSet的使用就可以解决这样的问题。在BitSet中,它是使用long类型来标志的。一个long变量长度是64位,每个位的值可以是0或者1,这样一个long类型变量最多就可以对64个数字(或者其他数据)进行标志。在BitSet中,维护的是一个long类型的数组。这样的话,针对数量很大的集合,就可以使用多个long元素来进行标志。同时这也意味着BitSet最少是一个long类型的长度。对于集合长度少于不大的,使用BitSet优势就没有那么明显了。
下面从“求1024里面的素数”方法来分析一下BitSet的使用。
//求1024之前的素数
private static void computerPrime(){
BitSet sieve = new BitSet(1024);
int size = sieve.size();
System.out.println(size);
for (int i = 2; i < size; i++) {
sieve.set(i);
}
System.out.println(size);
int finalBit = (int) Math.sqrt(size);
for (int i = 2; i < finalBit; i++) {
if (sieve.get(i)) {
for (int j = 2*i; j < size; j+=i) {
sieve.clear(j);
}
}
}
int counter = 0;
for (int i = 1; i < size; i++) {
if (sieve.get(i)){
System.out.printf("%5d",i);
if (++counter % 15 == 0){
System.out.println();
}
}
}
System.out.println();
}
首先创建了一个BitSet,通过查看BitSet的源码,可以发现BitSet对外提供两个构造函数。第一个默认构造函数初始化words数组的时候,给initWords方法传入的数值是64(BITS_PER_WORD是1左移6位之后的值,64)。而在initWords方法中传入wordIndex的值是63,经过在wordIndex方法中右移6位之后变成0,又加1,所以使用默认构造函数情况下,words的长度是1。第二个构造方法,需要传入指定的长度,这个长度值的作用是用来确定BitSet需要多少位进行数据的统计、筛选。如上面的computerPrime方法,求1024之前的素数个数,要对着1024个数字来进行标志需要1024位。那么1024位是多少个long呢?那就是1024/64=16,这是在刚好能被64整除的情况。正确的事从initWords和wordIndex中得到的是( ((1024-1) >> 6)+1),这样可以确保在不能被64整除的情况下,得到正确的words数组长度。
private final static int ADDRESS_BITS_PER_WORD = 6;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;//64
private long[] words;//long数组
private transient int wordsInUse = 0;//long数组中有几个元素被使用
public BitSet() {//默认构造函数
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
public BitSet(int nbits) {//传参构造函数
// nbits can't be negative; size 0 is OK
if (nbits < 0)
throw new NegativeArraySizeException("nbits < 0: " + nbits);
initWords(nbits);
sizeIsSticky = true;
}
private void initWords(int nbits) {//初始化数组
words = new long[wordIndex(nbits-1) + 1];
}
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
再看BitSet的set方法。首先判断确保传入的值要大于0,然后通过wordIndex方法,将传入的值右移6位,这个方法是用于查找传入的值的标志应该放在words数组的那个元素的索引(index)。比如如果传入的是0-63,这时候,右移6位之后,得到0,就会把这些值的标志放在words数组的第0个位置的元素,如果是64-127,右移6位之后,得到1,就会放在word的第一个元素上。expandTo方法是设置words数组用到了哪一个位置的元素,如果words数组长度不够,那就调用ensureCapacity方法延长words的长度。words[wordIndex] |= (1L << bitIndex);这一句是将words数组相应位置上的元素的位设置成相应的标志。
public void set(int bitIndex) {//set方法
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
expandTo(wordIndex);
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}
private static int wordIndex(int bitIndex) {//传入的值右移6位
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
private void expandTo(int wordIndex) {
//判断是不是用完了一个long变量的长度(64位),是的话,就从下一个变量开始设置
int wordsRequired = wordIndex+1;
if (wordsInUse < wordsRequired) {
ensureCapacity(wordsRequired);
wordsInUse = wordsRequired;
}
}
//如果words数组长度不够,则延长words数组的长度
private void ensureCapacity(int wordsRequired) {
if (words.length < wordsRequired) {
// Allocate larger of doubled size or required size
int request = Math.max(2 * words.length, wordsRequired);
words = Arrays.copyOf(words, request);
sizeIsSticky = false;
}
}
再看BitSet的get方法。首先判断确保传入的值要大于0,checkInvariants方法是用于检测各个变量是不是正常。然后通过wordIndex方法,获取bitIndex这个值的标志在words数组中的哪个位置(index)的元素上,最后返回bitIndex这个值的标志。
public boolean get(int bitIndex) {//get方法
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
private void checkInvariants() {//判断各个变量是否正常
assert(wordsInUse == 0 || words[wordsInUse - 1] != 0);
assert(wordsInUse >= 0 && wordsInUse <= words.length);
assert(wordsInUse == words.length || words[wordsInUse] == 0);
}
BitSet内部使用long而不是用其他类型来进行标志的存储是因为:BitSet内部提供了位运算(and、or),这两个操作需要对words中的位逐位运算。long是基本数据类型里面占用位数最多的数据类型,这样可以使得同等情况下,words数组长度最小,也就是循环次数最少(比如,初始化Bitset需要设置标志位长度为1024时,long类型,words的长度是16。如果words使用int类型,words长度将会是32)。循环次数越多,会导致性能下降。所以使用的是long。
使用BitSet排序:
private static void sort(){
int[] a = {12,54,63,10,8,5,6,3,5,4,7,456,8411,21,1};
BitSet bitSet = new BitSet();
for (int anA : a) {
bitSet.set(anA);
}
int k = 0;
int len = bitSet.cardinality();
int b[] = new int[len];
for (int i = bitSet.nextSetBit(0); i >=0 ; i = bitSet.nextSetBit(i+1)) {
b[k++] = i;
}
for (int aB : b) {
System.out.println(aB);
}
}