BitSet 转字节为何忽长忽短:固定标志字段需要自己的协议
协议要求一个四字节标志字段,new BitSet(32).toByteArray() 却返回了零字节。设置某个标志后长度又变成一,直到更高位出现才继续增长。这不是容量分配失败:构造参数影响内部容量,不会把尾部全零位变成必须输出的协议内容。
BitSet 的紧凑字节形式只保存到最高有效位所在的字节。它还采用低位在前的约定:位零对应第一个字节的最低位,位八对应第二个字节的最低位。若对端文档以“网络字节序”描述一个整数,不能直接认定两者相同。
下面明确约定一种自己的字段:位编号沿用 BitSet 规则,字段固定宽度,超出宽度及未使用高位都必须为零。用 Java 17 保存并运行 BitField.java:
import java.util.*;
public class BitField {
static int byteCount(int bits) {
if (bits < 1 || bits > 4096) throw new IllegalArgumentException("invalid width");
return (bits + 7) / 8;
}
static byte[] encode(BitSet flags, int bits) {
int bytes = byteCount(bits);
Objects.requireNonNull(flags);
if (flags.length() > bits) throw new IllegalArgumentException("bit outside field");
return Arrays.copyOf(flags.toByteArray(), bytes);
}
static BitSet decode(byte[] bytes, int bits) {
if (bytes.length != byteCount(bits)) throw new IllegalArgumentException("wrong byte count");
BitSet flags = BitSet.valueOf(bytes);
if (flags.length() > bits) throw new IllegalArgumentException("reserved bit set");
return flags;
}
public static void main(String[] args) {
BitSet flags = new BitSet(128);
System.out.println("empty-bytes=" + flags.toByteArray().length);
flags.set(0); flags.set(8);
System.out.println("compact=" + Arrays.toString(flags.toByteArray()));
byte[] fixed = encode(flags, 32);
System.out.println("fixed=" + Arrays.toString(fixed));
System.out.println("roundtrip=" + decode(fixed, 32).equals(flags));
flags.clear(8);
System.out.println("after-clear=" + Arrays.toString(flags.toByteArray()));
BitSet ten = new BitSet(); ten.set(9);
System.out.println("ten-bits=" + Arrays.toString(encode(ten, 10)));
try { decode(new byte[]{0, 4}, 10); }
catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
try { decode(new byte[]{1}, 32); }
catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
ten.set(10);
try { encode(ten, 10); }
catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
}
}
empty-bytes=0
compact=[1, 1]
fixed=[1, 1, 0, 0]
roundtrip=true
after-clear=[1]
ten-bits=[0, 2]
reserved bit set
wrong byte count
bit outside field
compact=[1, 1] 代表位零和位八。补成四字节后是 [1, 1, 0, 0],补零发生在数组尾部,因为这里遵循低位字节在前。把它左侧补零,虽然长度正确,却会把标志搬到另一个编号位置。
空 BitSet 输出长度零,与 new BitSet(128) 是否预留容量无关。清除最高位后紧凑输出也会缩短,正说明序列化长度由当前有效位决定。这里没有使用 size 作为协议字段宽度;size 描述实现的存储空间,不应成为跨系统合同。
固定宽度不等于静默裁掉高位
Arrays.copyOf 既能补零,也能截断。如果不先检查 flags.length,超出字段范围的高位会直接消失,调用方得到一个看似成功的结果。示例先拒绝越界,保证编码没有默默丢失标志。
解码也要求准确字节数。对一个四字节字段,只收到一字节不应自动补齐,因为那可能是上层报文截断;编码时主动补零和接收时宽容缺失,属于两个不同的协议决定。若系统确实使用可变长格式,应另行规定长度前缀和最大宽度,而不是让接收端猜。
十位字段占两个字节,但第二字节只有低两位可用,剩余六位是保留位。程序通过解码后的有效位长度检查它们,[0,4] 会被拒绝,因为它设置了位十。对将来版本预留的位,是拒绝、忽略还是转交新版本处理,也应由版本协议明确;当前程序采用严格拒绝。
byte 在 Java 中有符号,输出负数不代表某个标志“成了负位”。需要人工核对时,可以按 b & 0xff 或十六进制显示。比起观察十进制数组是否眼熟,更可靠的验证是挑几个明确编号的位,检查它们映射到哪个字节、哪个掩码,并同时测试全零与最高允许位。
在字段边界做拷贝与验证
本例编码得到新 byte 数组,解码也得到独立 BitSet;它们不依赖对方继续保持原数组内容。真实协议代码仍应控制谁能修改返回对象,尤其不要让多个请求共享同一个可变标志集合。BitSet 本身没有提供无条件的多线程修改安全。
宽度上限 4096 属于本例防止意外大分配的策略。若从报文中读取宽度,应先校验再做 (bits + 7) / 8 等计算,不能在不可信长度上先分配数组。固定协议则更简单:把宽度定义为明确常量,让长度错误在进入后续业务前停止。
最后拿本工具对接已有系统时,先拿到对端的位编号示例。单独写着“大端”或“小端”还不够说明字节内位序,文档里的第一个标志也可能按最高位编号。先对齐一组已知标志的真实字节,再复用这里的定长封装,才能避免双方都能编解码、跨语言却互不理解的情况。