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); } } }