JDK中的BitSet学习和整理

BitSet名字上看是一个Set,实际上可以看做是BitMap在JDK中的实现(JDK中没有BitMap这个类)

理解BitSet需要先了解下BitMap的设计

BitMap

直译就是位图,是一种数据结构,这种结构可以极大的节省存储空间

1 byte = 8 bit(就是1个字节等于8个比特位),一个bit可以表示成二进制中的1或者0两种值

这个BitMap的key就是元素,元素的值用bit值来标记

java中int类型占4字节,32bit位,最大值2^31-1=2147483647(20多亿)

需求场景:在20亿个整数中找出某个整数N是否存在,要求内存不能超过4G

正常思路,全部用整数存储,共需要的内存大小为:20亿*4/1024/1024/1024,约等于7.45G

如果使用bit存储,共需要20亿/8/1024/1024/1024,约等于7.45G/32

现在假设有一个字节数组,初始化长度为2  byte[] bitmap = new byte[2]

1字节=8位,如下图,底层是长度为16的bit数组

 

 然后想把2个键值对3-1,10-0存进去,可以这么设计

3/8=0    ----存在bitmap[0]上

3%8=3  ---存在bitmap[0]的第3个bit上,值为1

类似的

10/8=1 ---- 存在bitmap[1]上

10%8=2 ---存在bitmap[1]的第2个bit上,值为0

上面共可以存16个bit,如果有更多的需要存储,需要扩容,比如如果要存17-1,应该这样:

 

 这里是byte数组,每一个大格子可以存8位,如果是int,long,分别可以存32和64位

BitSet的核心源码分析

主要属性

// BitSet中的元素由words数组来组织,words是一个long型数组,long型变量由64位bit组成,64 = 2^6
private static final int ADDRESS_BITS_PER_WORD = 6;
 
// words中每个元素的比特数,也就是64 = 1 << 6
private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
 
// bit下标掩码:63
private static final int BIT_INDEX_MASK = BITS_PER_WORD - 1;
 
// 掩码,1个f是4个1,相当于64个1,用于部分word掩码的左移或者右移
private static final long WORD_MASK = 0xffffffffffffffffL;
/** * 真正的底层的比特存储结构,long数组 */ private long[] words;

 构造函数和核心方法

/**
 * 位索引转换成数组索引
 */
private static int wordIndex(int bitIndex) {
    return bitIndex >> 6;
}
/**
 * 根据位长度创建数组,相当于每64位为1个大格子,共需要多少个格子
 */
private void initWords(int nbits) {
    words = new long[wordIndex(nbits-1) + 1];
}
/**
 * 无参构造器,初始化的时候默认值都是false
 */
public BitSet() {
    initWords(64);
    sizeIsSticky = false;
}
/**
 * 根据位长度构造,默认值也是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;
}

注意:initWords方法表明,bitset的位大小一定是不小于指定初始大小的64的整数倍

/**
 * size是words数组的长度乘以64
 */
public int size() {
    return words.length * 64;
}
/**
 * length是bitset的最高位的位索引+1
 */
public int length() {
    if (wordsInUse == 0)
        return 0;

    return BITS_PER_WORD * (wordsInUse - 1) +
        (BITS_PER_WORD - Long.numberOfLeadingZeros(words[wordsInUse - 1]));
}
/**
 * 返回bitset中有为true的位有多少个
 */
public int cardinality() {
    int sum = 0;
    for (int i = 0; i < wordsInUse; i++)
        sum += Long.bitCount(words[i]);
    return sum;
}

比如,假设一个 BitSet 中存储了两个元素,10和50,此时这个 BitMap 的size = 64,length = 51,cardinality=2

/**
 * 动态扩容是2倍
 */
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;
    }
}

开源库

<dependency>
    <groupId>org.roaringbitmap</groupId>
    <artifactId>RoaringBitmap</artifactId>
    <version>0.9.23</version>
</dependency>

这个开源库是为了解决在数据比较稀疏的时候浪费空间的问题的,比如要存3个元素:1,1000,1000000,那初始化的时候需要申请的位长度是1000000

经典用法例子

有1000万个随机数字,数字的大小范围是0-1亿,要求找出0-1亿之间没有出现过的数字,尽量使用较小的内存

public static void main(String[] args) {
   Random random = new Random();
   BitSet bitSet = new BitSet(100000000);
   for (int i = 0; i < 10000000; i++) {
      bitSet.set(random.nextInt(100000000));
   }
   for (int i = 0; i < 100000000; i++) {
      if (!bitSet.get(i)) {
         System.out.println(i);
      }
   }
 }

 

posted @ 2022-08-03 16:04  鼠标的博客  阅读(233)  评论(0编辑  收藏  举报