java BitSet原理解析

       学习了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);
        }
}

猜你喜欢

转载自blog.csdn.net/lin962792501/article/details/81558433