揭秘 Java BitSet:深入源码剖析其使用原理
一、引言
在 Java 编程的世界里,数据结构扮演着至关重要的角色。它们就像一个个工具箱,为开发者提供了处理各种数据的有效手段。其中,BitSet 是一个独特且强大的数据结构,它以位(bit)为单位来存储和操作数据,在处理大量布尔值或需要进行位运算的场景中表现出色。
BitSet 可以被看作是一个动态的布尔数组,每个位(bit)可以存储一个布尔值,即 true 或 false。与普通的布尔数组不同,BitSet 的大小可以动态调整,并且提供了丰富的位操作方法,如设置位、清除位、翻转位等。这些特性使得 BitSet 在很多场景下都能发挥重要作用,例如数据压缩、布隆过滤器、位图索引等。
本文将深入探讨 Java BitSet 的使用原理,从源码的角度剖析其内部实现。我们将详细介绍 BitSet 的构造方法、核心操作方法以及一些高级用法,并通过实际的代码示例来帮助读者更好地理解。通过阅读本文,你将对 BitSet 有一个全面而深入的了解,能够在实际项目中更加灵活地运用它。
二、Java BitSet 概述
2.1 什么是 BitSet
BitSet 是 Java 标准库中的一个类,位于 java.util 包下。它实现了 Cloneable 和 java.io.Serializable 接口,意味着可以进行克隆和序列化操作。BitSet 类提供了一系列方法来操作位集合,允许用户方便地设置、清除、查询和操作位。
BitSet 的内部实现基于一个 long 类型的数组,每个 long 元素可以存储 64 位信息。这种设计使得 BitSet 能够高效地存储和操作大量的布尔值,同时节省内存空间。
2.2 BitSet 的应用场景
- 数据压缩:在处理大量布尔数据时,使用
BitSet可以显著减少内存占用。例如,在一个大型的用户系统中,需要记录每个用户的在线状态(在线或离线),使用BitSet可以将每个用户的状态用一个位来表示,从而大大节省内存。 - 布隆过滤器:布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。
BitSet可以作为布隆过滤器的底层存储结构,通过多个哈希函数将元素映射到BitSet的不同位上。 - 位图索引:在位图索引中,每个位代表一个数据记录的某个属性值。通过对
BitSet进行位运算,可以快速筛选出符合条件的数据记录。
三、BitSet 源码分析
3.1 类的定义和成员变量
// java.util.BitSet 类的定义,继承自 Object 类,实现了 Cloneable 和 Serializable 接口
public class BitSet implements Cloneable, java.io.Serializable {
// 用于存储位信息的 long 类型数组
private long[] words;
// 记录 words 数组中实际使用的元素数量
private transient int wordsInUse = 0;
// 标记是否需要检查 wordsInUse 的有效性
private transient boolean sizeIsSticky = false;
// 用于序列化和反序列化的版本号
private static final long serialVersionUID = 7997698588986878753L;
// 每个 long 类型元素可以存储的位数
private final static int ADDRESS_BITS_PER_WORD = 6;
// 每个 long 类型元素可以存储的位数的掩码
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
// 每个 long 类型元素可以存储的位数的掩码
private final static int BIT_INDEX_MASK = BITS_PER_WORD - 1;
// 用于检查数组是否越界的掩码
private static long WORD_MASK = 0xffffffffffffffffL;
}
在上述代码中,words 数组是 BitSet 的核心成员变量,用于存储位信息。wordsInUse 记录了 words 数组中实际使用的元素数量,sizeIsSticky 标记是否需要检查 wordsInUse 的有效性。ADDRESS_BITS_PER_WORD、BITS_PER_WORD 和 BIT_INDEX_MASK 是一些常量,用于计算位在数组中的位置。WORD_MASK 用于检查数组是否越界。
3.2 构造方法
3.2.1 默认构造方法
// 默认构造方法,创建一个初始大小为 64 位的 BitSet
public BitSet() {
// 调用另一个构造方法,初始大小为 64 位
initWords(BITS_PER_WORD);
// 标记不需要检查 wordsInUse 的有效性
sizeIsSticky = false;
}
默认构造方法创建了一个初始大小为 64 位的 BitSet。它调用了 initWords 方法来初始化 words 数组,并将 sizeIsSticky 标记为 false,表示不需要检查 wordsInUse 的有效性。
// 初始化 words 数组的方法
private void initWords(int nbits) {
// 计算需要的 long 类型元素数量
words = new long[wordIndex(nbits-1) + 1];
}
initWords 方法根据传入的位数计算需要的 long 类型元素数量,并创建相应大小的 words 数组。wordIndex 方法用于计算指定索引对应的 words 数组中的元素索引。
// 计算指定索引对应的 words 数组中的元素索引的方法
private static int wordIndex(int bitIndex) {
// 通过右移操作计算元素索引
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
wordIndex 方法通过右移操作将位索引转换为 words 数组中的元素索引。
3.2.2 指定初始大小的构造方法
// 指定初始大小的构造方法
public BitSet(int nbits) {
// 检查初始大小是否为负数
if (nbits < 0)
// 若为负数,抛出 IllegalArgumentException 异常
throw new NegativeArraySizeException("nbits < 0: " + nbits);
// 调用 initWords 方法初始化 words 数组
initWords(nbits);
// 标记需要检查 wordsInUse 的有效性
sizeIsSticky = true;
}
指定初始大小的构造方法允许用户指定 BitSet 的初始大小。在方法中,首先检查初始大小是否为负数,如果为负数则抛出 NegativeArraySizeException 异常。然后调用 initWords 方法初始化 words 数组,并将 sizeIsSticky 标记为 true,表示需要检查 wordsInUse 的有效性。
3.3 核心操作方法
3.3.1 设置位(set 方法)
// 设置指定索引的位为 true 的方法
public void set(int bitIndex) {
// 检查索引是否为负数
if (bitIndex < 0)
// 若为负数,抛出 IndexOutOfBoundsException 异常
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
// 计算指定索引对应的 words 数组中的元素索引
int wordIndex = wordIndex(bitIndex);
// 确保 words 数组有足够的空间
expandTo(wordIndex);
// 将指定索引的位设置为 1
words[wordIndex] |= (1L << bitIndex);
// 清除多余的高位
checkInvariants();
}
set 方法用于将指定索引的位设置为 true。首先检查索引是否为负数,如果为负数则抛出 IndexOutOfBoundsException 异常。然后计算指定索引对应的 words 数组中的元素索引,并调用 expandTo 方法确保 words 数组有足够的空间。接着使用位运算将指定索引的位设置为 1。最后调用 checkInvariants 方法清除多余的高位。
// 确保 words 数组有足够空间的方法
private void expandTo(int wordIndex) {
// 计算需要的 words 数组的大小
int wordsRequired = wordIndex + 1;
// 如果当前 wordsInUse 小于需要的大小
if (wordsInUse < wordsRequired) {
// 确保 words 数组有足够的空间
ensureCapacity(wordsRequired);
// 更新 wordsInUse 的值
wordsInUse = wordsRequired;
}
}
expandTo 方法用于确保 words 数组有足够的空间。它计算需要的 words 数组的大小,并与当前的 wordsInUse 进行比较。如果需要的大小大于当前的 wordsInUse,则调用 ensureCapacity 方法确保 words 数组有足够的空间,并更新 wordsInUse 的值。
// 确保 words 数组有足够容量的方法
private void ensureCapacity(int wordsRequired) {
// 如果 words 数组的长度小于需要的大小
if (words.length < wordsRequired) {
// 计算新的数组大小
int request = Math.max(2 * words.length, wordsRequired);
// 调用 Arrays.copyOf 方法扩容 words 数组
words = Arrays.copyOf(words, request);
// 标记不需要检查 wordsInUse 的有效性
sizeIsSticky = false;
}
}
ensureCapacity 方法用于确保 words 数组有足够的容量。如果 words 数组的长度小于需要的大小,则计算新的数组大小,并使用 Arrays.copyOf 方法扩容 words 数组。同时,将 sizeIsSticky 标记为 false,表示不需要检查 wordsInUse 的有效性。
// 清除多余高位的方法
private void checkInvariants() {
// 确保 wordsInUse 不超过 words 数组的长度
assert(wordsInUse == 0 || words[wordsInUse - 1] != 0);
// 确保 wordsInUse 不超过 words 数组的长度
assert(wordsInUse >= 0 && wordsInUse <= words.length);
// 确保 words 数组中 wordsInUse 之后的元素都为 0
assert(wordsInUse == words.length || words[wordsInUse] == 0);
}
checkInvariants 方法用于清除多余的高位。它通过断言确保 wordsInUse 的值在合理范围内,并且 words 数组中 wordsInUse 之后的元素都为 0。
3.3.2 清除位(clear 方法)
// 清除指定索引的位为 false 的方法
public void clear(int bitIndex) {
// 检查索引是否为负数
if (bitIndex < 0)
// 若为负数,抛出 IndexOutOfBoundsException 异常
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
// 计算指定索引对应的 words 数组中的元素索引
int wordIndex = wordIndex(bitIndex);
// 如果元素索引大于等于 wordsInUse,直接返回
if (wordIndex >= wordsInUse)
return;
// 将指定索引的位设置为 0
words[wordIndex] &= ~(1L << bitIndex);
// 清除多余的高位
recalculateWordsInUse();
// 清除多余的高位
checkInvariants();
}
clear 方法用于将指定索引的位设置为 false。首先检查索引是否为负数,如果为负数则抛出 IndexOutOfBoundsException 异常。然后计算指定索引对应的 words 数组中的元素索引,如果元素索引大于等于 wordsInUse,则直接返回。接着使用位运算将指定索引的位设置为 0。最后调用 recalculateWordsInUse 方法和 checkInvariants 方法清除多余的高位。
// 重新计算 wordsInUse 的方法
private void recalculateWordsInUse() {
// 从后往前遍历 words 数组
int i;
for (i = wordsInUse-1; i >= 0; i--)
// 找到第一个不为 0 的元素
if (words[i] != 0)
break;
// 更新 wordsInUse 的值
wordsInUse = i + 1;
}
recalculateWordsInUse 方法用于重新计算 wordsInUse 的值。它从后往前遍历 words 数组,找到第一个不为 0 的元素,并更新 wordsInUse 的值。
3.3.3 查询位(get 方法)
// 查询指定索引的位的值的方法
public boolean get(int bitIndex) {
// 检查索引是否为负数
if (bitIndex < 0)
// 若为负数,抛出 IndexOutOfBoundsException 异常
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
// 计算指定索引对应的 words 数组中的元素索引
int wordIndex = wordIndex(bitIndex);
// 如果元素索引大于等于 wordsInUse,返回 false
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
get 方法用于查询指定索引的位的值。首先检查索引是否为负数,如果为负数则抛出 IndexOutOfBoundsException 异常。然后计算指定索引对应的 words 数组中的元素索引,如果元素索引大于等于 wordsInUse,则返回 false。否则,使用位运算检查指定索引的位是否为 1。
3.4 位运算方法
3.4.1 与运算(and 方法)
// 对当前 BitSet 和另一个 BitSet 进行与运算的方法
public void and(BitSet set) {
// 如果当前 BitSet 和另一个 BitSet 是同一个对象,直接返回
if (this == set)
return;
// 从后往前遍历 words 数组
while (wordsInUse > set.wordsInUse)
// 将多余的元素置为 0
words[--wordsInUse] = 0;
// 对每个元素进行与运算
for (int i = 0; i < wordsInUse; i++)
words[i] &= set.words[i];
// 清除多余的高位
recalculateWordsInUse();
// 清除多余的高位
checkInvariants();
}
and 方法用于对当前 BitSet 和另一个 BitSet 进行与运算。如果两个 BitSet 是同一个对象,则直接返回。否则,从后往前遍历 words 数组,将多余的元素置为 0。然后对每个元素进行与运算。最后调用 recalculateWordsInUse 方法和 checkInvariants 方法清除多余的高位。
3.4.2 或运算(or 方法)
// 对当前 BitSet 和另一个 BitSet 进行或运算的方法
public void or(BitSet set) {
// 如果当前 BitSet 和另一个 BitSet 是同一个对象,直接返回
if (this == set)
return;
// 确保 words 数组有足够的空间
int wordsInUse = Math.max(this.wordsInUse, set.wordsInUse);
// 确保 words 数组有足够的空间
expandTo(wordsInUse);
// 对每个元素进行或运算
for (int i = 0; i < set.wordsInUse; i++)
words[i] |= set.words[i];
// 清除多余的高位
checkInvariants();
}
or 方法用于对当前 BitSet 和另一个 BitSet 进行或运算。如果两个 BitSet 是同一个对象,则直接返回。否则,计算需要的 wordsInUse 的值,并调用 expandTo 方法确保 words 数组有足够的空间。然后对每个元素进行或运算。最后调用 checkInvariants 方法清除多余的高位。
3.4.3 异或运算(xor 方法)
// 对当前 BitSet 和另一个 BitSet 进行异或运算的方法
public void xor(BitSet set) {
// 确保 words 数组有足够的空间
int wordsInUse = Math.max(this.wordsInUse, set.wordsInUse);
// 确保 words 数组有足够的空间
expandTo(wordsInUse);
// 对每个元素进行异或运算
for (int i = 0; i < set.wordsInUse; i++)
words[i] ^= set.words[i];
// 清除多余的高位
recalculateWordsInUse();
// 清除多余的高位
checkInvariants();
}
xor 方法用于对当前 BitSet 和另一个 BitSet 进行异或运算。它首先计算需要的 wordsInUse 的值,并调用 expandTo 方法确保 words 数组有足够的空间。然后对每个元素进行异或运算。最后调用 recalculateWordsInUse 方法和 checkInvariants 方法清除多余的高位。
3.5 其他方法
3.5.1 计算位数(length 方法)
// 计算 BitSet 中最高位为 1 的索引加 1 的方法
public int length() {
// 如果 wordsInUse 为 0,返回 0
if (wordsInUse == 0)
return 0;
// 计算最高位为 1 的索引
return BITS_PER_WORD * (wordsInUse - 1) +
(BITS_PER_WORD - Long.numberOfLeadingZeros(words[wordsInUse - 1]));
}
length 方法用于计算 BitSet 中最高位为 1 的索引加 1。如果 wordsInUse 为 0,则返回 0。否则,计算最高位为 1 的索引,并加上 BITS_PER_WORD * (wordsInUse - 1)。
3.5.2 计算设置为 1 的位数(cardinality 方法)
// 计算 BitSet 中设置为 1 的位数的方法
public int cardinality() {
// 初始化计数器
int sum = 0;
// 遍历 words 数组
for (int i = 0; i < wordsInUse; i++)
// 累加每个元素中设置为 1 的位数
sum += Long.bitCount(words[i]);
// 返回计数器的值
return sum;
}
cardinality 方法用于计算 BitSet 中设置为 1 的位数。它遍历 words 数组,使用 Long.bitCount 方法计算每个元素中设置为 1 的位数,并累加起来。
3.5.3 克隆方法(clone 方法)
// 克隆当前 BitSet 的方法
public Object clone() {
BitSet result;
try {
// 调用父类的 clone 方法
result = (BitSet) super.clone();
} catch (CloneNotSupportedException e) {
// 由于 BitSet 实现了 Cloneable 接口,不会抛出该异常
throw new InternalError(e);
}
// 复制 words 数组
result.words = words.clone();
// 清除多余的高位
result.checkInvariants();
// 返回克隆后的 BitSet
return result;
}
clone 方法用于克隆当前 BitSet。它首先调用父类的 clone 方法创建一个新的 BitSet 对象,然后复制 words 数组。最后调用 checkInvariants 方法清除多余的高位,并返回克隆后的 BitSet。
四、BitSet 的高级用法
4.1 布隆过滤器的实现
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。它通过多个哈希函数将元素映射到 BitSet 的不同位上。当查询一个元素时,检查对应的位是否都为 1,如果都为 1,则元素可能存在于集合中;如果有一个位为 0,则元素一定不存在于集合中。
import java.util.BitSet;
import java.util.Random;
// 布隆过滤器类
public class BloomFilter {
// 存储位信息的 BitSet
private BitSet bitSet;
// 哈希函数的数量
private int hashFunctions;
// 布隆过滤器的大小
private int size;
// 构造方法,初始化布隆过滤器
public BloomFilter(int size, int hashFunctions) {
// 初始化 BitSet
this.bitSet = new BitSet(size);
// 初始化哈希函数的数量
this.hashFunctions = hashFunctions;
// 初始化布隆过滤器的大小
this.size = size;
}
// 添加元素到布隆过滤器的方法
public void add(String element) {
// 遍历每个哈希函数
for (int i = 0; i < hashFunctions; i++) {
// 计算哈希值
int hash = hash(element, i);
// 将对应的位设置为 1
bitSet.set(hash);
}
}
// 检查元素是否可能存在于布隆过滤器的方法
public boolean mightContain(String element) {
// 遍历每个哈希函数
for (int i = 0; i < hashFunctions; i++) {
// 计算哈希值
int hash = hash(element, i);
// 如果对应的位为 0,返回 false
if (!bitSet.get(hash))
return false;
}
// 所有位都为 1,返回 true
return true;
}
// 计算哈希值的方法
private int hash(String element, int seed) {
// 使用随机数生成器计算哈希值
Random random = new Random(seed);
// 遍历元素的每个字符
for (char c : element.toCharArray()) {
// 更新随机数生成器的状态
random.setSeed(random.nextLong() + c);
}
// 返回哈希值
return Math.abs(random.nextInt()) % size;
}
public static void main(String[] args) {
// 创建布隆过滤器实例
BloomFilter bloomFilter = new BloomFilter(1000, 3);
// 添加元素
bloomFilter.add("apple");
bloomFilter.add("banana");
// 检查元素是否可能存在
System.out.println(bloomFilter.mightContain("apple")); // 输出: true
System.out.println(bloomFilter.mightContain("cherry")); // 输出: false
}
}
在上述代码中,BloomFilter 类实现了布隆过滤器的基本功能。add 方法用于将元素添加到布隆过滤器中,mightContain 方法用于检查元素是否可能存在于布隆过滤器中。hash 方法用于计算元素的哈希值。
4.2 位图索引的实现
位图索引是一种特殊的数据库索引,它使用 BitSet 来表示每个数据记录的某个属性值。通过对 BitSet 进行位运算,可以快速筛选出符合条件的数据记录。
import java.util.BitSet;
import java.util.HashMap;
import java.util.Map;
// 位图索引类
public class BitmapIndex {
// 存储属性值到位图的映射
private Map<String, BitSet> index;
// 数据记录的数量
private int recordCount;
// 构造方法,初始化位图索引
public BitmapIndex(int recordCount) {
// 初始化映射
this.index = new HashMap<>();
// 初始化数据记录的数量
this.recordCount = recordCount;
}
// 添加数据记录到位图索引的方法
public void addRecord(int recordIndex, String value) {
// 获取对应的位图
BitSet bitSet = index.computeIfAbsent(value, k -> new BitSet(recordCount));
// 将对应的位设置为 1
bitSet.set(recordIndex);
}
// 查询符合条件的数据记录的方法
public BitSet query(String value) {
// 返回对应的位图
return index.getOrDefault(value, new BitSet(recordCount));
}
public static void main(String[] args) {
// 创建位图索引实例
BitmapIndex bitmapIndex = new BitmapIndex(5);
// 添加数据记录
bitmapIndex.addRecord(0, "A");
bitmapIndex.addRecord(1, "B");
bitmapIndex.addRecord(2, "A");
bitmapIndex.addRecord(3, "C");
bitmapIndex.addRecord(4, "A");
// 查询符合条件的数据记录
BitSet result = bitmapIndex.query("A");
// 输出查询结果
for (int i = result.nextSetBit(0); i >= 0; i = result.nextSetBit(i + 1)) {
System.out.println("Record " + i + " has value A");
}
}
}
在上述代码中,BitmapIndex 类实现了位图索引的基本功能。addRecord 方法用于将数据记录添加到位图索引中,query 方法用于查询符合条件的数据记录。
五、BitSet 的性能分析
5.1 时间复杂度分析
- 设置位(set 方法):时间复杂度为 O(1),因为只需要计算位在
words数组中的位置,并进行位运算。 - 清除位(clear 方法):时间复杂度为 O(1),因为只需要计算位在
words数组中的位置,并进行位运算。 - 查询位(get 方法):时间复杂度为 O(1),因为只需要计算位在
words数组中的位置,并进行位运算。 - 与运算(and 方法):时间复杂度为 O(n),其中 n 是
words数组的长度。因为需要对每个元素进行与运算。 - 或运算(or 方法):时间复杂度为 O(n),其中 n 是
words数组的长度。因为需要对每个元素进行或运算。 - 异或运算(xor 方法):时间复杂度为 O(n),其中 n 是
words数组的长度。因为需要对每个元素进行异或运算。 - 计算位数(length 方法):时间复杂度为 O(1),因为只需要计算最高位为 1 的索引。
- 计算设置为 1 的位数(cardinality 方法):时间复杂度为 O(n),其中 n 是
words数组的长度。因为需要遍历每个元素并计算其中设置为 1 的位数。
5.2 空间复杂度分析
BitSet 的空间复杂度为 O(n/64),其中 n 是 BitSet 中存储的位数。因为每个 long 类型元素可以存储 64 位信息。
六、总结与展望
6.1 总结
Java BitSet 是一个功能强大且高效的数据结构,它以位为单位存储和操作数据,在处理大量布尔值或需要进行位运算的场景中表现出色。通过深入分析 BitSet 的源码,我们了解了它的内部实现原理,包括构造方法、核心操作方法、位运算方法等。
BitSet 的核心成员变量是一个 long 类型的数组,每个 long 元素可以存储 64 位信息。通过位运算,BitSet 可以高效地设置、清除和查询位。同时,BitSet 提供了丰富的位运算方法,如与运算、或运算、异或运算等,方便用户进行复杂的位操作。
在高级用法方面,BitSet 可以用于实现布隆过滤器和位图索引等数据结构。布隆过滤器可以用于判断一个元素是否存在于一个集合中,位图索引可以用于快速筛选出符合条件的数据记录。
6.2 展望
6.2.1 性能优化
虽然 BitSet 已经具有较高的性能,但在某些场景下仍然可以进一步优化。例如,可以通过优化位运算的实现,减少不必要的计算;可以采用更高效的内存管理策略,减少内存开销。
6.2.2 功能扩展
可以为 BitSet 添加更多的功能,如支持更多的位运算操作、提供更方便的批量操作方法等。同时,可以考虑将 BitSet 与其他数据结构进行结合,实现更复杂的功能。
6.2.3 应用场景拓展
随着计算机技术的不断发展,BitSet 的应用场景也将不断拓展。例如,在大数据、人工智能等领域,需要处理大量的布尔数据,BitSet 可以作为一种高效的数据存储和处理结构,为这些领域的应用提供支持。
总之,Java BitSet 是一个非常有价值的数据结构,通过不断的研究和优化,它将能够更好地满足各种复杂的应用需求。