前提#
本文主要内容是分析JDK
中的BitMap
实现之java.util.BitSet
的源码实现,基于JDK11
编写,其他版本的JDK
不一定合适。
文中的图比特低位实际应该是在右边,但是为了提高阅读体验,笔者把低位改在左边了。
什么是BitMap#
BitMap
,直译为位图,是一种数据结构,代表了有限域中的稠集(Dense Set
),每一个元素至少出现一次,没有其他的数据和元素相关联。在索引,数据压缩等方面有广泛应用(来源于维基百科词条)。计算机中1 byte = 8 bit
,一个比特(bit
,称为比特或者位)可以表示1
或者0
两种值,通过一个比特去标记某个元素的值,而KEY
或者INDEX
就是该元素,构成一张映射关系图。因为采用了Bit
作为底层存储数据的单位,所以可以极大地节省存储空间。
在Java
中,一个int
类型的整数占4
字节,16
比特,int
的最大值也就是20
多亿(具体是2147483647
)。假设现在有一个需求,在20
亿整数中判断某个整数m
是否存在,要求使用内存必须小于或者等于4GB
。如果每个整数都使用int
存储,那么存放20
亿个整数,需要20亿 * 4byte /1024/1024/1024
约等于7.45GB
,显然无法满足需求。如果使用BitMap
,只需要20亿 bit
内存,也就是20亿/8/1024/1024/1024
约等于0.233GB
。在数据量极大的情况下,数据集具备有限状态,可以考虑使用BitMap
存储和进行后续计算等处理。现在假设用byte
数组去做BitMap
的底层存储结构,初始化一个容量为16
的BitMap
实例,示例如下:
可见当前的byte
数组有两个元素bitmap[0]
(虚拟下标为[0,7]
)和bitmap[1]
(虚拟下标为[8,15]
)。这里假定使用上面构造的这个BitMap
实例去存储客户ID
和客户性别关系(比特为1
代表男性,比特为0
代表女性),把ID
等于3
的男性客户和ID
等于10
的女性客户添加到BitMap
中:
由于1 byte = 8 bit
,通过客户ID
除以8
就可以定位到需要存放的byte
数组索引,再通过客户ID
基于8
取模,就可以得到需要存放的byte
数组中具体的bit
的索引:
# ID等于3的男性客户
逻辑索引 = 3
byte数组索引 = 3 / 8 = 0
bit索引 = 3 % 8 = 3
=> 也就是需要存放在byte[0]的下标为3的比特上,该比特设置为1
# ID等于10的女性客户
逻辑索引 = 10
byte数组索引 = 10 / 8 = 1
bit索引 = 10 % 8 = 2
=> 也就是需要存放在byte[1]的下标为2的比特上,该比特设置为0
然后分别判断客户ID
为3
或者10
的客户性别:
如果此时再添加一个客户ID
为17
的男性用户,由于旧的BitMap
只能存放16
个比特,所以需要扩容,判断byte
数组中只需新增一个byte
元素(byte[2]
)即可:
原则上,底层的byte
数组可以不停地扩容,当byte
数组长度达到Integer.MAX_VALUE
,BitMap
的容量达到最大值。
BitSet简单使用#
java.util.BitSet
虽然名字上称为Set
,但实际上它就是JDK
中内置的BitMap
实现,1这个类算是一个十分古老的类,从注释上看是JDK1.0
引入的,不过大部分方法是JDK1.4
之后新添加或者更新的。以前一小节的例子基于BitSet
做一个Demo
:
public class BitSetApp {
public static void main(String[] args) {
BitSet bitmap = new BitSet(16);
bitmap.set(3, Boolean.TRUE);
bitmap.set(11, Boolean.FALSE);
System.out.println("Index 3 of bitmap => " + bitmap.get(3));
System.out.println("Index 11 of bitmap => " + bitmap.get(11));
bitmap.set(17, Boolean.TRUE);
// 这里不会触发扩容,因为BitSet中底层存储数组是long[]
System.out.println("Index 17 of bitmap => " + bitmap.get(17));
}
}
// 输出结果
Index 3 of bitmap => true
Index 11 of bitmap => false
Index 17 of bitmap => true
API
使用比较简单,为了满足其他场景,BitSet
还提供了几个实用的静态工厂方法用于构造实例,范围设置和清除比特值和一些集合运算等,这里不举例,后面分析源码的时候会详细展开。
BitSet源码分析#
前文提到,BitMap
如果使用byte
数组存储,当