java.util.zip可完成ZIP压缩解压但需手动处理流、编码、目录结构和中文路径;压缩时路径须标准化、递归处理目录、正确调用putNextEntry/closeEntry;解压需防路径穿越、校验合法性;中文名需统一UTF-8或按需fallback GBK;Jar类仅适用于Java应用打包。

Java 标准库的 java.util.zip 包能完成基础 ZIP 压缩与解压,无需第三方依赖,但必须手动处理流、编码、目录结构和中文路径问题——多数失败都卡在这四点上。
用 ZipOutputStream 压缩多个文件或目录
核心是把每个条目(ZipEntry)写入输出流,注意路径标准化和文件内容读取顺序:
-
ZipEntry的name必须用正斜杠/分隔,不能含盘符或开头的\或/(如"docs/readme.txt"合法,"C:\\docs\\readme.txt"或"/docs/readme.txt"会出错) - 压缩目录时需递归遍历,对子目录也创建一个带结尾
/的ZipEntry(如"images/"),否则解压后目录丢失 - 写入文件内容前必须先调用
putNextEntry(),且每个entry只能写一次;写完要调用closeEntry() - 若源文件含中文名,
ZipOutputStream默认使用 IBM437 编码,Windows 下需改用ZipOutputStream(OutputStream, Charset.forName("GBK"))(JDK 7+ 支持)
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("out.zip"), StandardCharsets.UTF_8)) {
addFileToZip(zos, Paths.get("src"), "");
} catch (IOException e) {
e.printStackTrace();
}
void addFileToZip(ZipOutputStream zos, Path file, String prefix) throws IOException {
String entryName = prefix + file.getFileName().toString();
if (Files.isDirectory(file)) {
zos.putNextEntry(new ZipEntry(entryName + "/"));
zos.closeEntry();
try (Stream stream = Files.list(file)) {
stream.forEach(child -> {
try {
addFileToZip(zos, child, entryName + "/");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
} else {
zos.putNextEntry(new ZipEntry(entryName));
Files.copy(file, zos);
zos.closeEntry();
}
}
用 ZipInputStream 安全解压 ZIP 文件
不能直接按 ZipEntry.getName() 创建 File,否则可能被路径穿越攻击(如 "../etc/passwd");同时要校验条目类型和目标路径合法性:
- 跳过
isDirectory()为true的条目(目录本身不包含数据,只靠路径名隐式存在) - 对每个
entry.getName()调用Paths.get(entry.getName()).normalize(),再检查是否仍以目标解压根目录为前缀 - 禁止解压到系统敏感路径(如
"C:\\"、"/etc/"),建议统一解压到临时子目录 - 读取内容时用
ZipInputStream.read(byte[])循环,不要一次性readAllBytes()——大文件会 OOM
Path targetDir = Paths.get("unzipped");
Files.createDirectories(targetDir);
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("in.zip"), StandardCharsets.UTF_8)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path targetFile = targetDir.resolve(entry.getName()).normalize();
// 防穿越:确保 targetFile 仍在 targetDir 下
if (!targetFile.startsWith(targetDir.toAbsolutePath().normalize())) {
throw new IOException("Bad zip entry: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(targetFile);
} else {
Files.createDirectories(targetFile.getParent());
Files.copy(zis, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
zis.closeEntry();
}
}
处理 ZIP 中的中文文件名乱码问题
根本原因是 ZIP 规范未强制指定编码,不同操作系统/工具默认不同:Windows 通常用 GBK,macOS/Linux 多用 UTF-8。JDK 7+ 的 ZipInputStream/ZipOutputStream 构造函数支持传入 Charset,但旧版 JDK(≤6)或某些 ZIP 工具生成的包仍可能不兼容:
立即学习“Java免费学习笔记(深入)”;
- 若已知 ZIP 由 Windows 上 7-Zip 或 WinRAR 生成,优先试
Charset.forName("GBK") - 若不确定,可先用
ZipInputStream读取条目名,按 UTF-8 解码失败后再用 GBK 尝试(需自己封装 fallback 逻辑) - 避免用
FileInputStream+ZipInputStream组合去“猜”编码——流已消费不可重置,应改用ByteArrayInputStream缓存原始字节再多次尝试 - 真正跨平台稳定的方案是:压缩端统一用 UTF-8(JDK 7+ 设置
StandardCharsets.UTF_8),解压端也用 UTF-8;若必须兼容老 ZIP,则需引入ant.jar的ZipFile(支持自动检测)或TrueZip
为什么不用 java.util.jar?
JarOutputStream 和 JarInputStream 是 ZipOutputStream/ZipInputStream 的子类,仅额外支持 META-INF/MANIFEST.MF。普通压缩解压完全没必要用它:
- 写 JAR 会强制在 ZIP 根目录加
META-INF/目录,哪怕你没提供清单文件 - 读 JAR 时若 ZIP 不含
META-INF/MANIFEST.MF,JarInputStream会抛IOException(而ZipInputStream不会) - 性能无差异,API 几乎一致,但语义混淆——除非你在打包 Java 应用,否则坚持用
zip相关类
最常被忽略的是:ZIP 条目名编码不是流的字符集,而是 ZIP 文件元数据的编码;ZipInputStream 构造时传的 Charset 只影响 getEntry().getName() 返回值,不影响文件内容解码——内容始终是原始字节流,该用什么编码读文件内容,和 ZIP 编码无关。










