这是《水煮 JDK 源码》系列 的第5篇文章,计划撰写100篇关于JDK源码相关的文章
GZIPOutputStream
类位于 java.util.zip
包下,继承于 DeflaterOutputStream
类,它实现了一个流式过滤器,主要用于以GZIP文件格式写入压缩数据,其UML类图如下:
类声明如下:
public class GZIPOutputStream extends DeflaterOutputStream
1、成员变量
GZIPOutputStream
定义了1个成员变量,如下:
/** CRC-32 用于未压缩的数据 */
protected CRC32 crc = new CRC32();
CRC32
是一个计算数据流的 CRC-32 校验和的类,主要用来验证压缩数据的完整性的。
2、构造函数
创建 GZIPOutputStream
压缩输出流主要有4种方式,如下:
public GZIPOutputStream(OutputStream out) throws IOException {
/** 使用默认大小的缓冲区创建新的输出流,默认大小为 512,同时不进行同步刷新 */
this(out, 512, false);
}
public GZIPOutputStream(OutputStream out, boolean syncFlush)
throws IOException
{
/** 使用默认大小的缓冲区创建新的输出流,默认大小为 512 */
this(out, 512, syncFlush);
}
public GZIPOutputStream(OutputStream out, int size) throws IOException {
/** 使用指定大小的缓冲区创建新的输出流,不进行同步刷新 */
this(out, size, false);
}
public GZIPOutputStream(OutputStream out, int size, boolean syncFlush)
throws IOException
{
/** 使用指定大小的缓冲区创建新的输出流 */
super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true),
size,
syncFlush);
// 使用默认的压缩器
usesDefaultDeflater = true;
// 写入 GZIP 成员头信息
writeHeader();
// 重置 CRC-32 校验
crc.reset();
}
GZIPOutputStream
类主要是用于写入GZIP压缩数据的输出流,因此需要用到压缩器 Deflater
。
3、写入数据方法
GZIPOutputStream
类提供了一个写入数据的方法,其定义如下:
public synchronized void write(byte[] buf, int off, int len)
throws IOException
{
// 调用父类 DeflaterOutputStream 的 write() 方法写入字节数组数据
super.write(buf, off, len);
// 更新字节数组数据的 CRC32 校验和
crc.update(buf, off, len);
}
在创建 GZIPOutputStream
压缩输出流的时候,会使用 writeHeader()
方法写入GZIP成员头信息,那么具体会写入哪些头信息呢?可以看看该方法的定义:
private void writeHeader() throws IOException {
// 这个 out 是定义在父类 FilterOutputStream 中的成员变量
out.write(new byte[] {
(byte) GZIP_MAGIC, // Magic number (short)
(byte)(GZIP_MAGIC >> 8), // Magic number (short)
Deflater.DEFLATED, // Compression method (CM)
0, // Flags (FLG)
0, // Modification time MTIME (int)
0, // Modification time MTIME (int)
0, // Modification time MTIME (int)
0, // Modification time MTIME (int)
0, // Extra flags (XFLG)
0 // Operating system (OS)
});
}
从代码实现可以看出,一共会写入10个字节的头信息,包括头信息魔法数、压缩方法和各种标志等,这些头信息在解压缩的时候也是需要一一校验的。
有写头信息的方法,自然就会有写尾信息的方法,关于写尾信息的 writeTrailer()
方法定义如下:
private void writeTrailer(byte[] buf, int offset) throws IOException {
// 写入 CRC32 的 value 值,int 型数据占用 4个字节
writeInt((int)crc.getValue(), buf, offset); // CRC-32 of uncompr. data
// 写入未压缩的字节总数
writeInt(def.getTotalIn(), buf, offset + 4); // Number of uncompr. bytes
}
上面调用的 writeInt()
方法定义如下:
private void writeInt(int i, byte[] buf, int offset) throws IOException {
// 在写入 int 数据时,是写入两次 short 型数据
writeShort(i & 0xffff, buf, offset);
writeShort((i >> 16) & 0xffff, buf, offset + 2);
}
private void writeShort(int s, byte[] buf, int offset) throws IOException {
// 而写入 short 数据时,时写入两次 byte 型数据
buf[offset] = (byte)(s & 0xff);
buf[offset + 1] = (byte)((s >> 8) & 0xff);
}
4、其他方法
当压缩的数据都写入到输出流中时,这个时候可以调用完成 finish()
方法,如下:
public void finish() throws IOException {
// 判断压缩数据是否完成,这个是调用压缩器 Deflater 的 finished() 方法
// 如果没有完成,则调用 Deflater 的 finish() 完成方法
if (!def.finished()) {
def.finish();
while (!def.finished()) {
// 压缩 buf 数据
int len = def.deflate(buf, 0, buf.length);
// 如果 def 已经完成,但是压缩后的数据长度小于减去尾部信息的长度
// 则写入尾部信息
if (def.finished() && len <= buf.length - TRAILER_SIZE) {
// last deflater buffer. Fit trailer at the end
writeTrailer(buf, len);
len = len + TRAILER_SIZE;
out.write(buf, 0, len);
return;
}
// 如果压缩后的数据长度大于 0,直接写入到输出流中
if (len > 0)
out.write(buf, 0, len);
}
// if we can't fit the trailer at the end of the last
// deflater buffer, we write it separately
// 单独的写入尾部信息,尾部信息的长度是 8个字节
byte[] trailer = new byte[TRAILER_SIZE];
writeTrailer(trailer, 0);
// 将尾部信息写入到
out.write(trailer);
}
}
5、测试应用
GZIPOutputStream
类可以用来压缩数据,那么下面给出一个压缩字符串的示例代码:
package com.magic.test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
public class GZIPOutputStreamTest {
public static void main(String[] args) {
String str = "0123456";
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(out)) {
// 将字符串 str 转换为字节数组后写入到 gzip 压缩输出流中
gzip.write(str.getBytes());
// 打印压缩后的数据
System.out.println(out.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行程序后,由于是压缩后的字符串,所以输出打印的是乱码。