Java常用压缩类库与ZipInputStream

应使用Apache Commons Compress的ZipArchiveInputStream并显式指定GBK等编码,或升级JDK至8u20+以支持ZIP64;避免用available()判断流结束,注意目录创建与路径穿越风险。

ZipInputStream读取中文文件名乱码怎么办

Java原生ZipInputStream不支持GBK、GB2312等中文编码,默认按UTF-8解码文件名,遇到老系统打包的ZIP(尤其是Windows下用WinRAR/7-Zip默认GBK)会返回?????.txt或抛IllegalArgumentException

解决思路不是“修复ZipInputStream”,而是绕过它——改用支持编码指定的第三方库,或在读取前预处理字节数组。JDK 7+ 的java.util.zip.ZipFile配合Charset参数仍不可用,真正可用的是:

  • org.apache.commons:commons-compress(推荐),其ZipArchiveInputStream允许传入Charset
  • net.sf.sevenzipjbinding(复杂,适合需要7z/LZMA场景)
  • 手动对ZipEntry.getName(

    ).getBytes(StandardCharsets.UTF_8)
    做编码转换(高风险,仅限已知原始编码且无特殊字符)

示例:用commons-compress读GBK编码ZIP

ZipArchiveInputStream zis = new ZipArchiveInputStream(
    new FileInputStream("test_gbk.zip"), 
    "GBK", // 显式指定编码
    true   // skipBytesForExtraField
);
ZipArchiveEntry entry;
while ((entry = zis.getNextZipEntry()) != null) {
    System.out.println(entry.getName()); // 正确输出中文名
    IOUtils.copy(zis, new FileOutputStream(entry.getName()));
}

ZipInputStream无法正确识别ZIP64扩展项

当ZIP文件中单个文件 > 4GB 或总条目数 > 65535,必须启用ZIP64格式。但JDK 6/7的ZipInputStream默认不识别ZIP64的central directory locator,会抛ZipException: invalid CEN header (invalid zip64 extra data)或直接跳过后续条目。

该问题在JDK 8u20+已修复,但仍有遗留环境运行旧JRE。验证方式:unzip -l broken.zip若提示zip64 end of central directory locator即为ZIP64。

  • JDK 8u20及以上:原生ZipInputStream可安全使用
  • JDK 7或更早:必须升级JRE,或改用commons-compress(从1.13起完整支持ZIP64)
  • 避免用ZipInputStream.available()判断流是否结束——它在ZIP64下始终返回0,应依赖getNextEntry() == null

ZipInputStream与ZipFile性能和资源管理差异

很多人误以为ZipInputStreamZipFile“更轻量”,其实相反:ZipInputStream是纯顺序读,无法随机访问;而ZipFile会将central directory加载进内存,支持getEntry("a/b.txt")直接定位,适合需多次查找特定文件的场景。

  • ZipInputStream:适合单次遍历、流式解压(如HTTP响应体直解)、内存受限环境
  • ZipFile:适合需随机读取、校验某几个文件、或提前获取所有条目元数据(size、time)的场景
  • ZipFile必须显式调用close(),否则底层RandomAccessFile句柄泄露;ZipInputStream也需关闭,但漏关只影响当前流
  • 二者都不支持边写边读ZIP——要生成ZIP请用ZipOutputStream

用ZipInputStream解压时跳过目录条目还是保留?

ZipInputStream读到的ZipEntry可能代表目录(entry.isDirectory() == true),也可能只是普通文件。是否创建对应目录,取决于你的业务逻辑。

  • 大多数解压工具(如unzip命令)默认创建目录结构,所以代码中应检查entry.isDirectory()mkdirs()
  • 若目标路径已存在同名文件,new File(entry.getName()).mkdirs()会静默失败,需提前delete()或跳过
  • 注意路径穿越风险:entry.getName()可能是../../etc/passwd,务必用FilenameUtils.normalize()(commons-io)或手动校验路径是否以"../"开头
  • 不要依赖entry.getSize() == 0判断目录——有些ZIP工具打空目录时会设非零size

真正的难点不在代码怎么写,而在你是否清楚这个ZIP是谁打的、用什么工具、在什么系统上、有没有隐藏属性——这些信息缺失时,光靠ZipInputStream本身无法还原原始意图。