BigInteger 读取字节数组变成负数:符号位与固定宽度怎样处理
- 发布时间:2026-10-23 23:03:35
- 本文热度:浏览 2 赞 0 评论 0
- 文章标签: Java BigInteger 二进制编码
- 全文共1字,阅读约需1分钟
字节80代表无符号128,new BigInteger(bytes)却得到-128。构造器并没有把字节读错,它按大端补码解释输入,最高位参与符号判断。
如果这段字节来自摘要、无符号字段或协议序号,问题在于调用方没有把“无符号”写进转换。BigInteger能表示大整数,不代表所有字节构造方式都自动忽略符号。
下面使用JDK 17,同时核对读入符号和写回宽度。保存为 UnsignedBytes.java,执行 java UnsignedBytes.java。
import java.math.BigInteger;
import java.util.HexFormat;
public class UnsignedBytes {
static byte[] fixed(BigInteger value, int width) {
if (width < 1 || width > 4096) throw new IllegalArgumentException("invalid width");
if (value.signum() < 0 || value.bitLength() > width * 8)
throw new IllegalArgumentException("unsigned value outside field range");
byte[] encoded = value.toByteArray(), result = new byte[width];
int n = Math.min(width, encoded.length);
System.arraycopy(encoded, encoded.length - n, result, width - n, n);
return result;
}
public static void main(String[] args) {
byte[] high = {(byte)0x80};
System.out.println("signed=" + new BigInteger(high));
BigInteger positive = new BigInteger(1, high);
System.out.println("unsigned=" + positive);
System.out.println("signed_encoding=" + HexFormat.of().formatHex(positive.toByteArray()));
byte[] four = fixed(positive, 4);
System.out.println("fixed=" + HexFormat.of().formatHex(four));
System.out.println("roundtrip=" + new BigInteger(1, four).equals(positive));
try { fixed(BigInteger.valueOf(256), 1); throw new AssertionError(); }
catch (IllegalArgumentException expected) { System.out.println("overflow=REJECTED"); }
try { fixed(BigInteger.valueOf(-1), 1); throw new AssertionError(); }
catch (IllegalArgumentException expected) { System.out.println("negative=REJECTED"); }
}
}
输出:
signed=-128
unsigned=128
signed_encoding=0080
fixed=00000080
roundtrip=true
overflow=REJECTED
negative=REJECTED
signum 只决定数值,不能保留协议宽度
两参数构造器显式传入1,把输入当作正的幅值。于是80得到128。原来有几个前导零,并不会作为整数的一部分永久保留;数值000080与80相同,转换成BigInteger以后就没有足够信息恢复原始宽度。
反方向的toByteArray又使用补码形式。正128需要一个前导零字节,避免最高位被当成负号,所以样本输出0080。这个前导零不是数值多了一位,而是有符号表示需要保留的符号信息。
因此“无符号读取”与“固定四字节写出”是两项独立协议。本例的fixed明确接收宽度,先检查非负与bitLength,再去掉必要符号前缀或在左侧补零。它不无条件截取最后几个字节,因为那会把超范围数值变成另一个合法小值。
范围检查在拷贝之前完成
256放不进一个无符号字节,样本拒绝,而不是生成00。负数也不允许被解释成某种补码无符号值。若协议真的规定按固定位宽取模,应提供另一个明确操作,不能把截断藏在通用编码器里。
width本身也限制范围,防止调用者为了编码一个很小的数字分配巨大数组。实际接口的宽度通常是协议常量,不必每次由远端传入。单位写清楚是字节,不是十六进制字符数,也不是位数。
示例输出四字节00000080,再用显式无符号构造恢复128,验证往返。测试还应包含0、单字节最高位为1、最大可表示值、刚超出一位和多字节前导零。只拿01做转换,不足以暴露符号位问题。
把转换放回一份完整报文
例如一份设备报文依次包含版本一字节、无符号序列号八字节、载荷长度两字节,最后才是载荷。都采用大端,但八字节序列号允许占满全部64位,不能用Java有符号long完整表示。这里选择BigInteger承接数值,同时让报文结构负责原始宽度。
保存下面的 UnsignedPacket.java 并运行。decode先检查最短头部,再按固定布局读取;长度字段与剩余载荷不一致就拒绝,不在一份截断报文上继续猜。
import java.math.BigInteger;
import java.nio.*;
import java.util.*;
public class UnsignedPacket {
record Packet(BigInteger sequence, byte[] payload) {}
static Packet decode(byte[] bytes) {
if (bytes.length < 11) throw new IllegalArgumentException("truncated header");
ByteBuffer in = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
if (Byte.toUnsignedInt(in.get()) != 1) throw new IllegalArgumentException("unknown version");
byte[] rawSequence = new byte[8]; in.get(rawSequence);
BigInteger sequence = new BigInteger(1,rawSequence);
int size = Short.toUnsignedInt(in.getShort());
if (in.remaining() != size) throw new IllegalArgumentException("payload length mismatch");
byte[] payload = new byte[size]; in.get(payload);
return new Packet(sequence,payload);
}
static byte[] encode(BigInteger sequence, byte[] payload) {
if (sequence.signum() < 0 || sequence.bitLength() > 64 || payload.length > 65535)
throw new IllegalArgumentException("field outside range");
byte[] signed = sequence.toByteArray(), fixed = new byte[8];
int n = Math.min(8,signed.length);
System.arraycopy(signed,signed.length-n,fixed,8-n,n);
return ByteBuffer.allocate(11+payload.length).order(ByteOrder.BIG_ENDIAN)
.put((byte)1).put(fixed).putShort((short)payload.length).put(payload).array();
}
public static void main(String[] args) {
byte[] raw = HexFormat.of().parseHex("01ffffffffffffffff0003414243");
Packet p = decode(raw);
System.out.println("sequence=" + p.sequence() + ", payload=" + p.payload().length);
System.out.println("packet roundtrip=" + Arrays.equals(raw,encode(p.sequence(),p.payload())));
System.out.println("small field=" + HexFormat.of().formatHex(encode(BigInteger.valueOf(128),new byte[0])));
try { p.sequence().longValueExact(); }
catch (ArithmeticException expected) { System.out.println("signed long=REJECTED"); }
try { decode(Arrays.copyOf(raw,raw.length-1)); }
catch (IllegalArgumentException e) { System.out.println(e.getMessage()); }
try { encode(BigInteger.ONE.shiftLeft(64),new byte[0]); }
catch (IllegalArgumentException e) { System.out.println("65-bit sequence=REJECTED"); }
}
}
sequence=18446744073709551615, payload=3
packet roundtrip=true
small field=0100000000000000800000
signed long=REJECTED
payload length mismatch
65-bit sequence=REJECTED
全FF的八字节序列号是18446744073709551615,超过有符号long最大值。longValueExact明确拒绝这次转换;若为了省事使用longValue,数值可能只留下低位,再次看起来像一个负编号。精确接收之后,后续存储或接口转换仍然要继续遵守范围,不是构造BigInteger成功就永远不会丢失。
encode使用既定八字节布局,序列号128仍写出足够的前导零。载荷长度用两个无符号字节表示,只有通过65535上限校验后才转成short写入;那次有符号Java类型转换仅承担位模式输出,不表示业务长度可以为负。读取时对应调用Short.toUnsignedInt,恢复协议含义。
字段级数值往返与报文级字节往返在这里都能核对。删除最后一个载荷字节会触发长度不符,65位序列号会在分配最终报文前失败。不要遇到超范围就截取末尾八字节,因为那会让另一个序列号冒充当前对象;也不要把缺失载荷自动补零,掩盖传输截断。
本例只接受单份完整报文,真实网络分块必须先按有上限的框架收齐相应字节,再调用decode;一次read没有填满缓冲区,不代表完整报文已经结束。Packet返回的payload仍是可变数组,作为业务模型继续传递时还需要约定所有权或做防御复制,这与无符号数值是否正确是两项独立保证。
摘要和令牌不一定应该转换成整数
如果目标只是把摘要显示为十六进制,HexFormat更直接,它保留每个字节对应的两位表示。把摘要先转BigInteger再toString(16),容易丢掉前导零;补回字符串宽度虽然可行,却增加了一层原本不需要的数值语义。
身份令牌也是如此:若协议要求比较原始字节,转换成数字可能把不同宽度编码合并为同一数值。应先决定身份按数值还是按字节串定义,再选择比较方式。密码学协议还可能要求专门的编码形式和常量时间操作,普通BigInteger转换不替代这些要求。
字节序也不能漏掉。本例按大端幅值处理,另一端若发送小端数据,需要按协议改变顺序,而不是看到负数就反转整个数组。符号、宽度和字节序分别回答三个问题,任意改一项都可能让小样本“碰巧正确”而大值出错。
这次修正的判断标准是明确的往返与超限拒绝:合法无符号值保留指定宽度,非法值不静默截断。只把构造器换成两参数,还不足以让整个二进制字段转换完成。