附录:数据压缩

Java I/O 类库提供了可以读写压缩格式流的类。你可以将其他 I/O 类包装起来用于提供压缩功能。

这些类不是从 ReaderWriter 类派生的,而是 InputStreamOutputStream 层级结构的一部分。这是由于压缩库处理的是字节,而不是字符。但是,你可能会被迫混合使用两种类型的流(请记住,你可以使用 InputStreamReaderOutputStreamWriter,这两个类可以在字节类型和字符类型之间轻松转换)。

压缩类 功能
CheckedInputStream getCheckSum() 可以对任意 InputStream 计算校验和(而不只是解压)
CheckedOutputStream getCheckSum() 可以对任意 OutputStream 计算校验和(而不只是压缩)
DeflaterOutputStream 压缩类的基类
ZipOutputStream DeflaterOutputStream 类的一种,用于压缩数据到 Zip 文件结构
GZIPOutputStream DeflaterOutputStream 类的一种,用于压缩数据到 GZIP 文件结构
InflaterInputStream 解压类的基类
ZipInputStream InflaterInputStream 类的一种,用于解压 Zip 文件结构的数据
GZIPInputStream InflaterInputStream 类的一种,用于解压 GZIP 文件结构的数据

尽管存在很多压缩算法,但是 Zip 和 GZIP 可能是最常见的。你可以使用许多用于读取和写入这些格式的工具,来轻松操作压缩数据。

使用 Gzip 简单压缩

GZIP 接口十分简单,因此当你有一个需要压缩的数据流(而不是一个包含不同数据分片的容器)时,使用 GZIP 更为合适。如下是一个压缩单个文件的示例:

  1. // compression/GZIPcompress.java
  2. // (c)2017 MindView LLC: see Copyright.txt
  3. // We make no guarantees that this code is fit for any purpose.
  4. // Visit http://OnJava8.com for more book information.
  5. // {java GZIPcompress GZIPcompress.java}
  6. // {VisuallyInspectOutput}
  7. public class GZIPcompress {
  8. public static void main(String[] args) {
  9. if (args.length == 0) {
  10. System.out.println(
  11. "Usage: \nGZIPcompress file\n" +
  12. "\tUses GZIP compression to compress " +
  13. "the file to test.gz");
  14. System.exit(1);
  15. }
  16. try (
  17. InputStream in = new BufferedInputStream(
  18. new FileInputStream(args[0]));
  19. BufferedOutputStream out =
  20. new BufferedOutputStream(
  21. new GZIPOutputStream(
  22. new FileOutputStream("test.gz")))
  23. ) {
  24. System.out.println("Writing file");
  25. int c;
  26. while ((c = in.read()) != -1)
  27. out.write(c);
  28. } catch (IOException e) {
  29. throw new RuntimeException(e);
  30. }
  31. System.out.println("Reading file");
  32. try (
  33. BufferedReader in2 = new BufferedReader(
  34. new InputStreamReader(new GZIPInputStream(
  35. new FileInputStream("test.gz"))))
  36. ) {
  37. in2.lines().forEach(System.out::println);
  38. } catch (IOException e) {
  39. throw new RuntimeException(e);
  40. }
  41. }
  42. }

使用压缩类非常简单,你只需要把你的输出流包装在 GZIPOutputStreamZipOutputStream 中,将输入流包装在 GZIPInputStreamZipInputStream。其他的一切就只是普通的 I/O 读写。这是面向字符流和面向字节流的混合示例;in 使用 Reader 类,而 GZIPOutputStreams 构造函数只能接受 OutputStream 对象,而不能接受 Writer 对象。当打开文件的时候,GZIPInputStream 会转换成为 Reader

使用 zip 多文件存储

支持 Zip 格式的库比 GZIP 库更广泛。有了它,你可以轻松存储多个文件,甚至还有一个单独的类可以轻松地读取 Zip 文件。该库使用标准 Zip 格式,因此它可以与当前可在 Internet 上下载的所有 Zip 工具无缝协作。以下示例与前一个示例具有相同的形式,但它可以根据需要处理任意数量的命令行参数。此外,它还显示了 Checksum 类计算和验证文件的校验和。有两种校验和类型:Adler32(更快)和 CRC32(更慢但更准确)。

  1. // compression/ZipCompress.java
  2. // (c)2017 MindView LLC: see Copyright.txt
  3. // We make no guarantees that this code is fit for any purpose.
  4. // Visit http://OnJava8.com for more book information.
  5. // Uses Zip compression to compress any
  6. // number of files given on the command line
  7. // {java ZipCompress ZipCompress.java}
  8. // {VisuallyInspectOutput}
  9. public class ZipCompress {
  10. public static void main(String[] args) {
  11. try (
  12. FileOutputStream f =
  13. new FileOutputStream("test.zip");
  14. CheckedOutputStream csum =
  15. new CheckedOutputStream(f, new Adler32());
  16. ZipOutputStream zos = new ZipOutputStream(csum);
  17. BufferedOutputStream out =
  18. new BufferedOutputStream(zos)
  19. ) {
  20. zos.setComment("A test of Java Zipping");
  21. // No corresponding getComment(), though.
  22. for (String arg : args) {
  23. System.out.println("Writing file " + arg);
  24. try (
  25. InputStream in = new BufferedInputStream(
  26. new FileInputStream(arg))
  27. ) {
  28. zos.putNextEntry(new ZipEntry(arg));
  29. int c;
  30. while ((c = in.read()) != -1)
  31. out.write(c);
  32. }
  33. out.flush();
  34. }
  35. // Checksum valid only after the file is closed!
  36. System.out.println(
  37. "Checksum: " + csum.getChecksum().getValue());
  38. } catch (IOException e) {
  39. throw new RuntimeException(e);
  40. }
  41. // Now extract the files:
  42. System.out.println("Reading file");
  43. try (
  44. FileInputStream fi =
  45. new FileInputStream("test.zip");
  46. CheckedInputStream csumi =
  47. new CheckedInputStream(fi, new Adler32());
  48. ZipInputStream in2 = new ZipInputStream(csumi);
  49. BufferedInputStream bis =
  50. new BufferedInputStream(in2)
  51. ) {
  52. ZipEntry ze;
  53. while ((ze = in2.getNextEntry()) != null) {
  54. System.out.println("Reading file " + ze);
  55. int x;
  56. while ((x = bis.read()) != -1)
  57. System.out.write(x);
  58. }
  59. if (args.length == 1)
  60. System.out.println(
  61. "Checksum: " + csumi.getChecksum().getValue());
  62. } catch (IOException e) {
  63. throw new RuntimeException(e);
  64. }
  65. // Alternative way to open and read Zip files:
  66. try (
  67. ZipFile zf = new ZipFile("test.zip")
  68. ) {
  69. Enumeration e = zf.entries();
  70. while (e.hasMoreElements()) {
  71. ZipEntry ze2 = (ZipEntry) e.nextElement();
  72. System.out.println("File: " + ze2);
  73. // ... and extract the data as before
  74. }
  75. } catch (IOException e) {
  76. throw new RuntimeException(e);
  77. }
  78. }
  79. }

对于要添加到存档的每个文件,必须调用 putNextEntry() 并传递 ZipEntry 对象。 ZipEntry 对象包含一个扩展接口,用于获取和设置 Zip 文件中该特定条目的所有可用数据:名称,压缩和未压缩大小,日期,CRC 校验和,额外字段数据,注释,压缩方法以及它是否是目录条目。但是,即使 Zip 格式有设置密码的方法,Java 的 Zip 库也不支持。虽然 CheckedInputStreamCheckedOutputStream 都支持 Adler32 和 CRC32 校验和,但 ZipEntry 类仅支持 CRC 接口。这是对基础 Zip 格式的限制,但它可能会限制你使用更快的 Adler32。

要提取文件,ZipInputStream 有一个 getNextEntry() 方法,这个方法在有文件存在的情况下调用,会返回下一个 ZipEntry。作为一个更简洁的替代方法,你可以使用 ZipFile 对象读取该文件,该对象具有方法 entries() 返回一个包裹 ZipEntriesEnumeration

要读取校验和,你必须以某种方式访问关联的 Checksum 对象。这里保留了对 CheckedOutputStreamCheckedInputStream 对象的引用,但你也可以保持对 Checksum 对象的引用。 Zip 流中的一个令人困惑的方法是 setComment()。如 ZipCompress 所示。在 Java 中,你可以在编写文件时设置注释,但是没有办法恢复 ZipInputStream 中的注释。注释似乎仅通过 ZipEntry 在逐个条目的基础上完全支持。

使用 GZIP 或 Zip 库时,你不仅被限制于文件——你可以压缩任何内容,包括通过网络连接发送的数据。

Java 的 jar

Zip 格式也用于 JAR(Java ARchive)文件格式,这是一种将一组文件收集到单个压缩文件中的方法,就像 Zip 一样。但是,与 Java 中的其他所有内容一样,JAR 文件是跨平台的,因此你不必担心平台问题。你还可以将音频和图像文件像类文件一样包含在其中。

JAR 文件由一个包含压缩文件集合的文件和一个描述它们的“清单(manifest)”组成。(你可以创建自己的清单文件;否则,jar 程序将为你执行此操作。)你可以在 JDK 文档中,找到更多关于 JAR 清单的信息。

JDK 附带的 jar 工具会自动压缩你选择的文件。你可以在命令行上调用它:

  1. jar [options] destination [manifest] inputfile(s)

选项是一组字母(不需要连字符或任何其他指示符)。 Unix / Linux 用户会注意到这些选项与 tar 命令选项的相似性。这些是:

选项 功能
c 创建一个新的或者空的归档文件
t 列出内容目录
x 提取所有文件
x file 提取指定的文件
f 这代表着,“传递文件的名称。”如果你不使用它,jar 假定它的输入将来自标准输入,或者,如果它正在创建一个文件,它的输出将转到标准输出。
m 代表第一个参数是用户创建的清单文件的名称。
v 生成详细的输出用于表述 jar 所作的事情
0 仅存储文件;不压缩文件(用于创建放在类路径中的 JAR 文件)。
M 不要自动创建清单文件

如果放入 JAR 文件的文件中包含子目录,则会自动添加该子目录,包括其所有子目录等。还会保留路径信息。

以下是一些调用 jar 的典型方法。以下命令创建名为 myJarFile 的 JAR 文件。 jar 包含当前目录中的所有类文件,以及自动生成的清单文件:

  1. jar cf myJarFile.jar *.class

下一个命令与前面的示例类似,但它添加了一个名为 myManifestFile.mf 的用户创建的清单文件。 :

  1. jar cmf myJarFile.jar myManifestFile.mf *.class

这个命令输出了 myJarFile.jar 中的文件目录:

  1. jar tf myJarFile.jar

如下添加了一个“verbose”的标志,用于生成更多关于 myJarFile.jar 中文件的详细信息:

  1. jar tvf myJarFile.jar

假设 audio,classes 和 image 都是子目录,它将所有子目录组合到文件 myApp.jar 中。还包括“verbose”标志,以便在 jar 程序工作时提供额外的反馈:

  1. jar cvf myApp.jar audio classes image

如果你在创建 JAR 文件时使用了 0(零) 选项,该文件将会被替换在你的类路径(CLASSPATH)中:

  1. CLASSPATH="lib1.jar;lib2.jar;"

然后 Java 可以搜索到 lib1.jar 和 lib2.jar 的类文件。

jar 工具不像 Zip 实用程序那样通用。例如,你无法将文件添加或更新到现有 JAR 文件;只能从头开始创建 JAR 文件。

此外,你无法将文件移动到 JAR 文件中,在移动文件时将其删除。

但是,在一个平台上创建的 JAR 文件可以通过任何其他平台上的 jar 工具透明地读取(这个问题有时会困扰 Zip 实用程序)。