开始前,会这些就够了
- 知道压缩数据与解压后的内容是两组字节
- 能读 while 循环和
ByteBuffer的 position、remaining - 先读 解压内容已经读出来,为什么最后一步仍会失败?:先理解看见正文与完整格式验证的区别,再学习Inflater如何交还当前段之后的输入。
一块输入里先放压缩后的 HELLO,再接上普通文字 NEXT。Inflater 已经返回 finished=true,可 needsInput=false,getRemaining() 还显示四个字节。究竟哪个状态才代表读完?
这三个状态分别回答不同问题:当前压缩段是否结束、当前交给它的输入是否耗尽、还有多少输入没消费。本文固定一个 zlib 段,用真实 JDK 输出把这几条边界拆开。
把例子带到自己的电脑上
下载完整练习包
需要 完整 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 的目录打开终端。命令分别回车执行。真实Deflater/Inflater,默认zlib包装;每JDK53检查,每版复跑3次一致。仅内存输入,未测raw DEFLATE/GZIP/ZIP、预置字典、恶意输入或网络。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
正文出现、压缩段结束、输入用完,是三个观察项
正在准备实验控件…
四组结果均来自本页 Java 程序在 JDK 17/21 的实际输出;浏览器不执行解压。固定 HELLO 与 NEXT、默认 zlib 包装,没有覆盖任意压缩内容、预置字典或网络读取。
一块输入,可以装着不止一个逻辑段
本例先用默认 Deflater 把 ASCII 文本 HELLO 编成 zlib 格式,再把四个普通字节 NEXT 接到后面。NEXT 是我们在实验里约定的后续字段,不属于前一个 zlib 数据段,也没有再次压缩。
RFC 1950 的格式说明把 ADLER32 校验值之后的数据排除在当前 zlib 段之外。因此,一次把整块数组交给 Inflater,不意味着它应当吞掉这块数组里的所有内容。外层协议仍需要安排下一段由谁读取。
finished 和 needsInput 不是互为相反数
finished=true 表示已到达当前压缩数据段的结尾;needsInput=true 只表示当前输入区没有剩余字节。exact 控制组恰好只给一段完整压缩数据,两者可以同时为 true。
with-tail 控制组还附带 NEXT。解压段完成后 finished=true,但输入尚余四字节,所以 needsInput=false。不能写成“只要还不需要输入,就继续把数据当作当前压缩段处理”。
header-only 只送入两个头字节,得到 finished=false、needsInput=true。此时需要的是更多输入;如果外层已经确认真正结束,就应报告这段数据不完整,而不是把“输入区暂时空了”写成成功。
让 ByteBuffer 的位置告诉下一步从哪里接手
给 setInput(ByteBuffer) 传入缓冲区后,inflate 会随着消费推进它的 position。实验结束时,位置恰好停在压缩段之后,remaining 为 4;从当前位置取出的四字节正是 NEXT。
程序在调用 end 之前保存并检查这些状态,然后把后续字节交给外层逻辑。若使用 byte[] 重载,要结合本次传入区间的终点与 getRemaining 计算尾部起点;不能无视原始 offset,始终从数组的总长度倒着推。
这个演示只说明后续字节被保留,没有定义 NEXT 的真实协议含义。完整通信协议还需要明确长度、字段类型和错误处理规则。
看到了完整 HELLO,也不能跳过结束状态
另一个控制组拿掉压缩数据最后两个字节。解压器仍产出完整 HELLO,但 finished=false,needsInput=true。这个特定截断发生在末尾部分,展示的是“内容已出现,数据段尚未完整结束”。
Inflater 本身没有这里使用的“再也不会有输入”参数。是否还会到来更多字节,要由调用方根据文件结束、消息长度或上层协议判断。若上层已结束,而 finished 仍为 false,业务就不能把暂时拿到的正文作为完整成功结果。
每次拿到几个字节,都要继续查看状态
inflate 返回本次写入输出区的字节数。实验分别用容量 1、2、8 的输出数组反复取结果,再组合成 HELLO,证明不能把一次调用当成必定产生全部内容。
drain 在 finished、needsInput 或 needsDictionary 出现时停下,供调用方区分下一步。若没产出字节且没有任何上述状态,教学代码直接报错,避免静默空转;固定样本还设有 1000 次迭代上限。
这里没有预置字典样本,所以没有宣称字典恢复已验证;DataFormatException 也直接交由 main 报错。生产输入还需要明确输出大小预算,本实验的循环保护不能替代完整的资源限制。
运行四个状态对照和十五种尾部组合
Main.java 只使用标准库,数据全部来自内存;Deflater 与 Inflater 都在 finally 中调用 end 释放资源。完整压缩输出由循环收集,代码不假设它的长度或压缩字节排列在所有运行时都相同。
2026-10-24,在 Windows、Microsoft OpenJDK 17.0.20.1 与 21.0.12 分别以 --release 17 编译,每版运行三次;六份 stdout 统一换行后相同,每次通过 53 项检查。矩阵包含三种输出容量与零到四个尾字节。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class Main {
private static int checks;
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static byte[] compress(byte[] input) {
Deflater encoder = new Deflater();
try {
encoder.setInput(input);
encoder.finish();
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] block = new byte[16];
while (!encoder.finished()) {
int count = encoder.deflate(block);
if (count == 0 && !encoder.finished()) throw new AssertionError("encoder stalled");
out.write(block, 0, count);
}
return out.toByteArray();
} finally { encoder.end(); }
}
private static byte[] drain(Inflater decoder, int blockSize) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] block = new byte[blockSize];
for (int guard = 0; guard < 1000; guard++) {
int count = decoder.inflate(block);
out.write(block, 0, count);
if (decoder.finished() || decoder.needsInput() || decoder.needsDictionary()) return out.toByteArray();
if (count == 0) throw new AssertionError("decoder made no progress");
}
throw new AssertionError("fixture iteration limit");
}
private static byte[] append(byte[] first, byte[] second) {
byte[] joined = Arrays.copyOf(first, first.length + second.length);
System.arraycopy(second, 0, joined, first.length, second.length);
return joined;
}
public static void main(String[] args) throws Exception {
byte[] payload = "HELLO".getBytes(StandardCharsets.US_ASCII);
byte[] encoded = compress(payload);
byte[] tail = "NEXT".getBytes(StandardCharsets.US_ASCII);
Inflater decoder = new Inflater();
try {
decoder.setInput(encoded, 0, 2);
byte[] first = drain(decoder, 8);
check(first.length == 0 && !decoder.finished() && decoder.needsInput(), "header alone is incomplete");
System.out.println("header-only: output=" + first.length + " finished=" + decoder.finished()
+ " needsInput=" + decoder.needsInput() + " remaining=" + decoder.getRemaining());
decoder.setInput(encoded, 2, encoded.length - 2);
check(Arrays.equals(drain(decoder, 1), payload) && decoder.finished(), "feed remaining input");
check(decoder.getRemaining() == 0 && decoder.needsInput(), "complete input exactly consumed");
System.out.println("exact: output=HELLO finished=" + decoder.finished()
+ " needsInput=" + decoder.needsInput() + " remaining=" + decoder.getRemaining());
decoder.reset();
decoder.setInput(Arrays.copyOf(encoded, encoded.length - 2));
byte[] truncated = drain(decoder, 8);
check(Arrays.equals(truncated, payload) && !decoder.finished() && decoder.needsInput(), "payload is not stream completion");
System.out.println("truncated: output=HELLO finished=" + decoder.finished()
+ " needsInput=" + decoder.needsInput() + " remaining=" + decoder.getRemaining());
decoder.reset();
ByteBuffer framed = ByteBuffer.wrap(append(encoded, tail));
decoder.setInput(framed);
check(Arrays.equals(drain(decoder, 2), payload) && decoder.finished(), "first stream finished");
check(decoder.getRemaining() == 4 && !decoder.needsInput(), "tail remains");
check(framed.position() == encoded.length && framed.remaining() == 4, "byte buffer cursor identifies tail");
byte[] recoveredTail = new byte[framed.remaining()];
framed.get(recoveredTail);
check(Arrays.equals(recoveredTail, tail), "next protocol field preserved");
System.out.println("with-tail: output=HELLO finished=true needsInput=false remaining=4 tail="
+ new String(recoveredTail, StandardCharsets.US_ASCII));
} finally { decoder.end(); }
for (int blockSize : new int[]{1, 2, 8}) for (int tailLength = 0; tailLength <= 4; tailLength++) {
Inflater current = new Inflater();
try {
byte[] suffix = Arrays.copyOf(tail, tailLength);
current.setInput(append(encoded, suffix));
check(Arrays.equals(drain(current, blockSize), payload), "output block size does not lose bytes");
check(current.finished() && current.getRemaining() == tailLength, "tail length preserved");
check(current.needsInput() == (tailLength == 0), "needsInput is about available bytes");
} finally { current.end(); }
}
System.out.println("matrix: 3 output sizes x 5 tail lengths checked");
System.out.println("PASS " + checks + " checks; one zlib stream, no files or network I/O");
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-24 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。真实Deflater/Inflater,默认zlib包装;每JDK53检查,每版复跑3次一致。仅内存输入,未测raw DEFLATE/GZIP/ZIP、预置字典、恶意输入或网络。
运行后对照结果
header-only: output=0 finished=false needsInput=true remaining=0
exact: output=HELLO finished=true needsInput=true remaining=0
truncated: output=HELLO finished=false needsInput=true remaining=0
with-tail: output=HELLO finished=true needsInput=false remaining=4 tail=NEXT
matrix: 3 output sizes x 5 tail lengths checked
PASS 53 checks; one zlib stream, no files or network I/O
这一段结束后,外层协议继续决定接下来读什么
保存输出、判断压缩段是否完整、保留后续输入,是三件独立的工作。如果协议只允许一段压缩内容,尾部数据也许应被拒绝;如果后面还有合法字段,就应按外层格式继续解析。这是调用方的约定,不由 Inflater 自动决定。
本例仅用默认 zlib 包装和固定 ASCII 内容,没有测试 raw DEFLATE、GZIP 多成员、ZIP 容器、预置字典、网络分包或恶意压缩输入。不要把这份小实验当成通用解压服务。
记住这三点
- finished 关心当前压缩段结尾,
needsInput关心当前输入区是否为空。 getRemaining与ByteBuffer.position可帮助保留压缩段后的数据。- 完整正文已出现,仍需检查压缩段真正结束及外层输入边界。
别把例子用过头
- 把
needsInput=true直接当作解压完成。 - 完成一个压缩段后丢掉缓冲区里的后续字段。
- 把 inflate 返回零或本次有输出当成唯一结束条件。
- 只调用一次 deflate 或 inflate 就假设收齐所有结果。
自己试一小步
如果压缩段后面还有一个六字节字段,你怎样既确认前一段完整,又保证下一段不被吞掉?
- 把 NEXT 换成另一个固定字段,先写下预期剩余字节数。
- 分别用 byte[] 和
ByteBuffer传入,记录初始范围、已消费量和后续字段起点。 - 改变输出区容量,比较最终正文、finished 与剩余尾部是否一致。
- 另做“删去尾部压缩字节”与“合法压缩段后增加字段”两组,区分截断与额外输入。
做到这些,就算完成
- 仅在外层确认输入结束且压缩段完整时报告成功。
- 后续字段逐字节一致,不凭字符串看起来相似判断。
- 练习不附答案,报告中写清所用格式与尚未验证的输入。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java 17 Inflater:完成状态与剩余输入 (新标签页)↗
finished、
needsInput、getRemaining,以及setInput(ByteBuffer)随消费推进 position 的约定。 - Java 17 Deflater:finish 与收集压缩输出 (新标签页)↗
默认产生 zlib 格式;finish 请求结束,finished 判断输出结束,end 释放资源。
- RFC 1950 §2.2:zlib 段的边界 (新标签页)↗
zlib 结构以 ADLER32 结束,其后数据不属于当前 zlib 段。