开始前,会这些就够了
- 认识十六进制和 byte 数组
- 知道一个字节有八位,接收方需要按约定解释它们
- 先读 同样四个字节,为什么读出两个数字?:先把字节顺序与数值解释分开,再增加符号和宽度这两个条件。
接口规定金额编号占一个无符号字节,数值 128 明明没有超过 255;可 BigInteger.valueOf(128).toByteArray() 却给出 00 80 两个字节。直接取最后一个字节后,再用普通构造器读回,还变成了 -128。
本实验把三个约定拆开:有无符号、占几字节、从哪一端开始排列。新增的 00 是符号表示的一部分,不能只因为看起来多余就随手删掉。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。真实BigInteger字节表示与1..32字节自定义无符号导出;每JDK65,583检查,含全部65,536个两字节值与宽字段边界。未实现密码/网络协议,未穷举更宽字段或测试性能。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
00 是符号位,还是固定宽度的填充?
正在准备实验控件…
浏览器用 −65,536 到 65,536 的整数、1–4 字节宽度模拟编码约定,所有整数运算都在精确范围内。真实 BigInteger 与本文 1–32 字节导出函数由下方 Java 程序验证;这不是网络协议实现。
127 到 128,多出的一字节在说明正负
toByteArray 返回能表示这个 BigInteger 的最短二补码字节序列,并且至少保留一个符号位。它采用大端顺序:前面的字节更高位。
127 的表示是 7F,最高位为 0,一个字节已经足够;128 若只写成 80,作为有符号八位值就会被解释成 -128,所以它需要前面的 00,得到 00 80。255 同样得到 00 FF。
负数也有对应边界:-128 只需 80,-129 要用 FF 7F。零的表示是 00,并不是空数组。主程序逐项核对字节内容,再用配套的有符号构造器读回原值。
同一份 FF,换一种构造器就有另一种含义
new BigInteger(bytes) 把输入当作大端二补码。因此只含 FF 的数组得到 -1;new BigInteger(1, bytes) 把符号单独指定为正,再把数组当作大小,得到 255。
Java 的 byte 本身取值为 -128 到 127,不影响它保存八位原始数据。本例用 HexFormat 每字节显示两位十六进制,便于看清 80、FF;需要按无符号字节作算术核对时使用 b & 0xFF。
编码端和解码端必须选同一约定。若自定义字段规定无符号一字节,80 可以表示 128,但接收方不能再不加说明地套用有符号的 BigInteger(byte[])。
数值相等,不代表原来那几个补零字节会留下来
把 00 00 7F 交给有符号构造器,得到的是数值 127。再调用 toByteArray,只会得到最短表示 7F;BigInteger 不为你记住输入曾经占三个字节。
如果协议要求固定两字节,那么 127 应写成 00 7F,128 写成 00 80。这时前面的零既可能满足固定宽度,也可能在有符号表示中承担符号含义;看见相同的字节不能反推两边采用了同一种协议。
所以固定字段宽度需要作为单独参数保存,不应靠 toByteArray 的长度决定。字段中的前导零能否去掉,要由该字段格式决定,不能只看整数数值有没有变化。
先限定非负范围,再生成固定宽度的无符号字段
unsignedFixed 是本文自定义的小函数,约定数值非负、宽度为 1 到 32 字节、大端排列。先检查 value.bitLength() 是否超过 width*8,再分配结果,避免把放不下的高位静默截掉。
只有通过非负和范围检查后,才识别并跳过 toByteArray 可能增加的单个 00 符号字节。其余大小字节右对齐复制到新数组,左侧由新数组的零补足。不是对任意带符号输入无条件砍掉第一字节。
256 放不进一个无符号字节,负数也不符合本函数约定,都明确拒绝;宽度 0 和 33 同样拒绝。1 到 32 是教学接口的资源上限,不是 BigInteger 官方限制。
运行完整程序:每套 JDK 核对 65,583 项条件
保存唯一的 Main.java,编译后运行。程序使用 BigInteger 与 Java 17 引入的 HexFormat,不依赖第三方库、网络、密钥或数据库。所有数值都是代码里公开生成的教学数据。
2026-10-23,在 Windows、Microsoft OpenJDK 17.0.20.1 与 21.0.12 上分别以 --release 17 编译并实跑,stdout 统一换行后完全相同;每版最终输出 PASS 65583。
其中 65,536 项遍历所有无符号两字节数值,既与 value >>> 8 和 value & 0xFF 的独立位拆分结果比较,也用无符号构造器读回。其他检查覆盖符号边界、填充、空输入及更宽字段上限;这是有限域正确性核对,不是性能测量。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.math.BigInteger;
import java.util.Arrays;
import java.util.HexFormat;
public final class Main {
private static int checks;
private static final HexFormat HEX = HexFormat.ofDelimiter(" ").withUpperCase();
private static void check(boolean ok, String message) {
checks++;
if (!ok) throw new AssertionError(message);
}
private static void expect(Class<? extends Throwable> type, Runnable action) {
try {
action.run();
} catch (Throwable failure) {
check(type.isInstance(failure), "Wrong exception: " + failure);
return;
}
throw new AssertionError("Expected " + type.getSimpleName());
}
// Application policy: unsigned, big-endian, exactly 1..32 bytes.
private static byte[] unsignedFixed(BigInteger value, int width) {
if (value == null || width < 1 || width > 32) {
throw new IllegalArgumentException("value and width 1..32 are required");
}
if (value.signum() < 0 || value.bitLength() > width * 8) {
throw new IllegalArgumentException("value does not fit the unsigned field");
}
byte[] signed = value.toByteArray();
int skip = signed.length > 1 && signed[0] == 0 ? 1 : 0;
int length = signed.length - skip;
byte[] field = new byte[width];
System.arraycopy(signed, skip, field, width - length, length);
return field;
}
public static void main(String[] args) {
int[] numbers = {-129, -128, -1, 0, 1, 127, 128, 255, 256};
String[] encoded = {"FF 7F", "80", "FF", "00", "01", "7F", "00 80", "00 FF", "01 00"};
for (int i = 0; i < numbers.length; i++) {
BigInteger number = BigInteger.valueOf(numbers[i]);
byte[] bytes = number.toByteArray();
check(HEX.formatHex(bytes).equals(encoded[i]), "minimal signed representation");
check(new BigInteger(bytes).equals(number), "signed round trip");
System.out.println("signed " + number + " -> " + HEX.formatHex(bytes)
+ " -> " + new BigInteger(bytes));
}
byte[] ff = {(byte) 0xFF};
check(new BigInteger(ff).equals(BigInteger.valueOf(-1)), "byte[] constructor is signed");
check(new BigInteger(1, ff).equals(BigInteger.valueOf(255)), "positive magnitude is unsigned");
System.out.println("same FF: signed=" + new BigInteger(ff) + "; positive-magnitude=" + new BigInteger(1, ff));
byte[] paddedInput = {0, 0, 0x7F};
byte[] minimalAgain = new BigInteger(paddedInput).toByteArray();
check(HEX.formatHex(minimalAgain).equals("7F"), "original padding is not retained");
System.out.println("padding: " + HEX.formatHex(paddedInput) + " -> " + HEX.formatHex(minimalAgain));
expect(NumberFormatException.class, () -> new BigInteger(new byte[0]));
check(new BigInteger(1, new byte[0]).equals(BigInteger.ZERO), "empty magnitude represents zero");
expect(NumberFormatException.class, () -> new BigInteger(0, new byte[]{1}));
expect(NumberFormatException.class, () -> new BigInteger(2, new byte[0]));
System.out.println("empty: signed=NumberFormatException; positive-magnitude=0");
for (int value : new int[]{0, 127, 128, 255}) {
byte[] oneByte = unsignedFixed(BigInteger.valueOf(value), 1);
check(oneByte.length == 1 && (oneByte[0] & 0xFF) == value, "one-byte unsigned field");
check(new BigInteger(1, oneByte).intValueExact() == value, "matching unsigned decoder");
System.out.println("unsigned-width1 " + value + " -> " + HEX.formatHex(oneByte));
}
byte[] twoBytes = unsignedFixed(BigInteger.valueOf(128), 2);
check(Arrays.equals(twoBytes, new byte[]{0, (byte) 0x80}), "width is an explicit contract");
check(new BigInteger(unsignedFixed(BigInteger.valueOf(128), 1)).intValueExact() == -128,
"wrong signed decoder changes the meaning");
System.out.println("unsigned-width2 128 -> " + HEX.formatHex(twoBytes));
expect(IllegalArgumentException.class, () -> unsignedFixed(BigInteger.valueOf(256), 1));
expect(IllegalArgumentException.class, () -> unsignedFixed(BigInteger.valueOf(-1), 1));
expect(IllegalArgumentException.class, () -> unsignedFixed(BigInteger.ZERO, 0));
expect(IllegalArgumentException.class, () -> unsignedFixed(BigInteger.ZERO, 33));
System.out.println("rejected: 256-in-width1; negative-value; width0; width33");
int roundTrips = 0;
for (int value = 0; value <= 65535; value++) {
byte[] bytes = unsignedFixed(BigInteger.valueOf(value), 2);
check(bytes.length == 2 && (bytes[0] & 0xFF) == (value >>> 8)
&& (bytes[1] & 0xFF) == (value & 0xFF)
&& new BigInteger(1, bytes).intValueExact() == value,
"two-byte oracle and round trip");
roundTrips++;
}
for (int width : new int[]{4, 8, 16, 32}) {
BigInteger max = BigInteger.ONE.shiftLeft(width * 8).subtract(BigInteger.ONE);
byte[] bytes = unsignedFixed(max, width);
check(bytes.length == width && new BigInteger(1, bytes).equals(max), "wide maximum round trip");
expect(IllegalArgumentException.class, () -> unsignedFixed(max.add(BigInteger.ONE), width));
}
System.out.println("unsigned-width2-oracle=" + roundTrips + "; wide-boundaries=4,8,16,32");
System.out.println("PASS " + checks);
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。真实BigInteger字节表示与1..32字节自定义无符号导出;每JDK65,583检查,含全部65,536个两字节值与宽字段边界。未实现密码/网络协议,未穷举更宽字段或测试性能。
运行后对照结果
signed -129 -> FF 7F -> -129
signed -128 -> 80 -> -128
signed -1 -> FF -> -1
signed 0 -> 00 -> 0
signed 1 -> 01 -> 1
signed 127 -> 7F -> 127
signed 128 -> 00 80 -> 128
signed 255 -> 00 FF -> 255
signed 256 -> 01 00 -> 256
same FF: signed=-1; positive-magnitude=255
padding: 00 00 7F -> 7F
empty: signed=NumberFormatException; positive-magnitude=0
unsigned-width1 0 -> 00
unsigned-width1 127 -> 7F
unsigned-width1 128 -> 80
unsigned-width1 255 -> FF
unsigned-width2 128 -> 00 80
rejected: 256-in-width1; negative-value; width0; width33
unsigned-width2-oracle=65536; wide-boundaries=4,8,16,32
PASS 65583
先观察具体字节,再核对接收端的解释
signed 128 一行是 00 80;unsigned-width1 128 一行是 80;unsigned-width2 128 又是 00 80。三行数值相同,但分别由最短有符号表示、固定一字节与固定两字节格式产生。
same FF 一行把输入保持不变,只更换解释方式,输出 -1 和 255。padding 一行则把数值保持不变,展示原始冗余补零会被最短表示去掉。
empty 一行单独区分两种构造器:空二补码数组抛 NumberFormatException,空的正号 magnitude 可表示零。本例自己的固定宽度接口始终要求至少一个字节,不能把标准库允许空 magnitude 的行为自动当成业务协议允许空字段。
字节序、符号和宽度要分别写进接口约定
已有字节序文章解释相同字节排列如何影响数值;本篇保持大端不变,专门核对有符号解释与字段宽度。若对接小端协议,需要另行转换并测试,不能认为增加或删掉一个 00 就解决了字节序。
代码还验证 4、8、16、32 字节的最大无符号值,以及再加 1 必须拒绝,但没有穷举这些巨大输入域。读取任意外部字节前,还需要根据业务验证允许长度、格式版本与空值约定。
已验证两套 JDK 上的标准库编码及本页受限导出函数。没有实现网络报文、数据库驱动或密码协议,也不宣称不同协议都允许相同补零策略。
记住这三点
toByteArray是最短有符号二补码,不是任意协议的固定宽度无符号字段。- 相同 FF 在有符号和显式正号 magnitude 构造器中分别表示 -1 与 255。
- 固定宽度要独立记录,超过范围应拒绝而不是截掉高位。
别把例子用过头
- 对任何结果都删除第一个 00 或只取末尾几字节。
- 把
BigInteger保留数值误认为它还会保留原始补零长度。 - 编码按无符号、解码却用默认有符号构造器。
- 看到 Java byte 打印为负数,就断言原始八位已经损坏。
自己试一小步
为一个固定三字节的非负编号字段写出编码与解码合同,你怎样证明没有偷偷丢位?
- 先列出允许范围、字节序、宽度和零值格式,不运行代码。
- 设计涉及最高位从 0 变 1、字段最大值和刚超上限的输入,并预测完整十六进制字节。
- 比较有符号构造器与显式正号 magnitude 构造器,记录它们在哪些输入上含义不同。
- 为原始输入含额外前导零的情况制定政策,保存数值比较与字节比较两份结果。
做到这些,就算完成
- 边界值与超界值分别覆盖。
- 字节数、每一字节和回读值都检查。
- 没有把只测试小范围写成已穷举全部大整数。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java SE 17 BigInteger:字节构造器、toByteArray 与 bitLength (新标签页)↗
二补码与显式符号大小的区别、最短表示包含符号位,以及空数组与长度检查。
- Java 语言规范 17:整数类型与数值 (新标签页)↗
Java byte 是八位有符号二补码,范围为 -128 到 127。
- Java SE 17 HexFormat:按字节显示十六进制 (新标签页)↗
每字节格式化为两位十六进制,用于观察原始字节而不混入 byte 的十进制符号。