bitset适合内存敏感的布尔标记场景,如上亿连续整数的存在性标记;不适合存字符串、稀疏id或替代hashset。其核心优势是1位存1个布尔值,但需注意扩容陷阱、线程不安全、序列化兼容性等问题。

BitSet 适合什么场景:内存敏感的布尔标记,不是通用集合
BitSet 的核心价值是用 1 位(bit)存一个 true/false,而不是像 boolean[] 那样至少占 1 字节。当你要标记上百万甚至上亿个整数是否存在、是否访问过、是否被选中,且这些整数范围集中(比如 ID 在 0~5000 万之间),BitSet 才真正有优势。它不适合存字符串、对象或稀疏大跨度 ID(比如只有 ID=1 和 ID=999999999),也不该用来替代 HashSet 做任意元素去重。
常见错误现象:BitSet.set(1000000000) 后内存暴涨、GC 频繁;或者误把 BitSet 当成线程安全集合,在多线程里直接共享修改导致状态错乱。
- 使用场景典型例子:用户行为埋点去重(同一天内某用户是否点击过某广告)、布隆过滤器底层存储、图算法中标记已访问节点(节点 ID 连续且可控)
- 不适用场景:需要按插入顺序遍历、要存非整数键、需支持
null或动态扩容到极稀疏范围 - 性能影响:
get()和set()是 O(1),但cardinality()(统计 true 个数)是 O(n/64),因为要遍历内部 long 数组
初始化和扩容陷阱:size 不等于实际内存占用
BitSet 没有“容量”构造参数,只有 new BitSet() 或 new BitSet(int size)。后者只是预设内部数组长度(以 bit 为单位),但不会限制你调用 set(n) 设置远超该值的索引——BitSet 会自动扩容。这容易让人误以为传了 1000 就只占 1000 bit,其实只要 set(1000000),内部就可能分配几十个 long(每个 64 bit)。
错误示例:BitSet bs = new BitSet(100); bs.set(1000); —— 看似只申请了 100 bit,实际用了约 16 个 long(1024 bit),且之前预分配的 100 bit 完全没用上。
立即学习“Java免费学习笔记(深入)”;
- 建议:如果知道最大索引上限(比如用户 ID 最大是 49999999),直接用
new BitSet(50000000),避免频繁扩容带来的数组复制开销 - 注意:BitSet 内部用
long[]存储,所以真实内存占用 ≈((maxIndex + 64) / 64) * 8字节(+64 是向上取整) - 兼容性:Java 7+ 行为一致,但早期版本
length()返回的是最高 set 位的索引+1,不是实际分配大小,别拿它当“已用空间”
去重逻辑怎么写:别直接用 BitSet 当 Set
BitSet 本身不提供“添加元素并判断是否已存在”的原子操作,必须手动检查再设置。很多人想当然地 bs.set(id); 就完事,结果重复 ID 无法识别,达不到去重目的。
正确做法是先 get(id) 判断,再决定是否处理。尤其在流式去重(如日志解析)中,这个判断不能省。
if (!bs.get(userId)) {
bs.set(userId);
// 处理新用户逻辑
}- 注意
get(i)对未 set 过的索引返回false,这是安全的默认行为 - 不要用
bs.length() 来判断越界——<code>length()返回的是“逻辑长度”,即最高 true 位位置+1,和是否 set 过无关;越界检查应靠业务层保证id >= 0 - 并发场景下,
get()+set()非原子,必须加锁或改用AtomicIntegerArray等替代方案
序列化与跨系统传输:BitSet 不是通用数据格式
BitSet 的 toByteArray() 返回的是紧凑的字节数组,但它是 JVM 内部表示(高位在前、按 long 分块填充),不同语言或旧版 Java 反序列化时极易出错。直接把它当网络协议字段或存进数据库 blob,后续维护成本很高。
错误现象:BitSet.valueOf(bytes) 在另一台机器上还原出完全不同的位模式;或 MySQL 中存了 byte[],读出来后 cardinality() 为 0。
- 跨语言传输建议:转成十六进制字符串(
String.format("%x", bs.toLongArray()))或 Base64 编码,接收方按规范解析 - 持久化建议:存为整数数组(
bs.stream().toArray())或拆成多个小范围 BitSet 分表存储,别依赖二进制字节 - 兼容性坑:Java 8 引入了
toLongArray(),但老版本只能用toByteArray(),两者布局不同,升级时要校验
位运算本身很高效,但 BitSet 的边界条件比看上去复杂得多——索引是否越界、内存是否虚高、序列化是否可逆,这三个地方最容易在线上突然暴露问题。










