系列中的上一篇
当前教程
装饰 IO 流
系列中的下一篇

系列中的上一篇: 读取和写入二进制文件

系列中的下一篇: 内存中的 IO 流

装饰 IO 流

 

装饰的目的

装饰器模式是四人帮提出的 23 种设计模式之一。Java I/O API 使用此模式来扩展或修改其某些类的行为。

The Reader 类层次结构说明了如何使用装饰来设计 Java I/O。

The Reader Class Hierarchy

Reader 类层次结构

The Reader 类是一个抽象类,它定义了可以完成的字符读取操作。它由三个具体类扩展:CharArrayReaderStringReader(未在此图中显示)和 FileReader,它们提供了一个读取字符的媒介。

然后 BufferedReader 扩展 Reader 并对其进行装饰。要创建 BufferedReader 的实例,您必须提供一个 Reader 对象,该对象充当 BufferedReader 对象的委托。然后,BufferedReader 类向基本 Reader 类添加了几个方法。

BufferedReader 类的装饰允许覆盖 Reader 类的现有具体方法,以及添加新方法。

对于 LineNumberReader 类也是如此,它扩展了 BufferedReader 并且需要一个 Reader 实例才能被构造。

 

向二进制流写入和读取字符

您在本节的介绍中看到,Java I/O API 的类分为两类,一类用于处理字符,另一类用于处理字节。尝试从文本文件读取或写入字节是没有意义的。但是,将字符写入二进制文件是应用程序中广泛使用的操作。

Java I/O API 为此提供了两个类

InputStreamReaderReader 类的装饰,建立在 InputStream 对象之上。如果需要,您可以提供字符集。对于 OutputStreamWriter 类也是如此,它扩展了 Writer 并且需要一个 OutputStream 对象才能被构建。

使用 OutputStreamWriter 写入字符

让我们使用 OutputStreamWriter 将消息写入文本文件。

String message = """
        From fairest creatures we desire increase,
        That thereby beauty's rose might never die,
        But as the riper should by time decease
        His tender heir might bear his memory:
        But thou, contracted to thine own bright eyes,
        Feed'st thy light's flame with self-substantial fuel,
        Making a famine where abundance lies,
        Thyself thy foe, to thy sweet self too cruel.
        Thou that art now the world's fresh ornament,
        And only herald to the gaudy spring,
        Within thine own bud buriest thy content,
        And, tender churl, mak'st waste in niggardly.
        Pity the world, or else this glutton be,
        To eat the world's due, by the grave and thee.""";

Path path = Path.of("files/sonnet.txt");
try (var outputStream = Files.newOutputStream(path);
     var writer = new OutputStreamWriter(outputStream);) {

    writer.write(message);

} catch (IOException e) {
    e.printStackTrace();
}

long size = Files.size(path);
System.out.println("size = " + size);

运行此代码将在 files 目录中创建一个名为 sonnet.txt 的文件,其中包含莎士比亚第一首十四行诗的文本。

此示例中值得注意的是以下几点。

  • The OutputStreamWriter 是通过装饰使用 Files 类中的工厂方法创建的 OutputStream 来创建的。
  • 输出流和写入器都作为 try-with-resources 模式的参数创建,从而确保它们将以正确的顺序被刷新和关闭。如果您遗漏了这一点,您的文件中可能会出现缺失的字符,仅仅是因为内部缓冲区没有被正确刷新。

运行此代码将显示以下结果。

size = 609

使用 InputStreamReader 读取字符

读取您在上一节中创建的 sonnet.txt 文件遵循相同的模式。以下是代码。

Path path = Path.of("files/sonnet.txt");
String sonnet = null;
try (var inputStream = Files.newInputStream(path);
     var reader = new InputStreamReader(inputStream);
     var bufferedReader = new BufferedReader(reader);
     Stream<String> lines = bufferedReader.lines();) {

    sonnet = lines.collect(Collectors.joining("\n"));

} catch (IOException e) {
    e.printStackTrace();
}

System.out.println("sonnet = \n" + sonnet);

reader 对象是通过装饰 inputStream 对象创建的,就像之前一样。不过,此代码更进一步。

  • 它装饰了这个普通的 reader 对象以创建一个 BufferedReader。The BufferedReader 类有几个方法可以逐行读取文本文件,我们将在本示例中使用这些方法。
  • 它在 BufferedReader 对象上调用 lines() 方法。此方法返回此文本文件的行流。由于流实现了 AutoCloseable,因此您可以将其作为 try-with-resources 模式的参数创建。

使用 Collectors.joining() 收集器收集流是一种非常简单的方法,可以将此流的所有元素连接起来,并用换行符(在本示例中)分隔。

运行此代码将产生以下结果。

sonnet =
From fairest creatures we desire increase,
That thereby beauty's rose might never die,
But as the riper should by time decease
His tender heir might bear his memory:
But thou, contracted to thine own bright eyes,
Feed'st thy light's flame with self-substantial fuel,
Making a famine where abundance lies,
Thyself thy foe, to thy sweet self too cruel.
Thou that art now the world's fresh ornament,
And only herald to the gaudy spring,
Within thine own bud buriest thy content,
And, tender churl, mak'st waste in niggardly.
Pity the world, or else this glutton be,
To eat the world's due, by the grave and thee.

 

处理压缩的二进制流

装饰器模式以非常有效的方式用于读取和写入 gzip 文件。Gzip 是 deflate 算法的一种实现。此格式在 RFC 1952 中指定。JDK 中有两个类实现了此算法:GZIPInputStreamGZIPOutputStream.

这两个类是基本类 InputStreamOutputStream 的扩展。它们只是覆盖了字节的读取和写入,而没有添加任何方法。装饰在这里用于覆盖默认行为。

由于装饰器模式,修改这两个之前的示例以将此文本写入压缩文件只是对代码进行了一些小的修改。

使用 GzipOutputStream 写入数据

以下是您可以用来将文本写入 gzip 文件的代码。

String message = ...; // the same sonnet as previously
Path path = Path.of("files/sonnet.txt.gz");
try (var outputStream = Files.newOutputStream(path);
     var gzipOutputStream = new GZIPOutputStream(outputStream);
     var writer = new OutputStreamWriter(gzipOutputStream);) {

    writer.write(message);

} catch (IOException e) {
    e.printStackTrace();
}

long size = Files.size(path);
System.out.println("size = " + size);

请注意,gzipOutputStream 对象是通过装饰常规的 outputStream 创建的,并用于创建 writer 对象。代码中没有其他更改。

由于此文件现在已压缩,因此其大小更小。运行此代码将显示以下内容。

size = 377

请注意,您可以使用任何能够读取 gzip 文件的软件打开此文件。

使用 GzipInputStream 读取数据

以下代码读取回文本。

Path path = Path.of("files/sonnet.txt.gz");
String sonnet = null;
try (var inputStream = Files.newInputStream(path);
     var gzipInputStream = new GZIPInputStream(inputStream);
     var reader = new InputStreamReader(gzipInputStream);
     var bufferedReader = new BufferedReader(reader);
     var stream = bufferedReader.lines();) {

    sonnet = stream.collect(Collectors.joining("\n"));

} catch (IOException e) {
    e.printStackTrace();
}

System.out.println("sonnet = \n" + sonnet);

请注意,gzipInputStream 对象是通过装饰常规的 inputStream 创建的。然后,此 gzipInputStream 对象被装饰以创建 reader 对象。代码的其余部分保持不变。

 

处理基本类型流

Java I/O API 提供了 InputStreamOutputStream 的另外两种装饰:DataInputStreamDataOutputStream.

这些类添加了在二进制流上读取和写入基本类型的方法。

写入基本类型

The DataOutputStream 类将其所有写入操作委托给它包装的 OutputStream 实例。此类提供以下方法来写入基本类型

  • writeByte(int): 将参数的八个低位写入底层流。参数的 24 个高位被忽略。

这些其他方法是不言自明的。

The DataOutputStream 类还提供了从数组写入字节和字符的方法。

以下代码将 6 个 int 写入二进制文件。

int[] ints = {3, 1, 4, 1, 5, 9};
Path path = Path.of("files/ints.bin");
try (var outputStream = Files.newOutputStream(path);
     var dataOutputStream = new DataOutputStream(outputStream);) {

    for (int i : ints) {
        dataOutputStream.writeInt(i);
    }

} catch (IOException e) {
    e.printStackTrace();
}
System.out.printf("Wrote %d ints to %s [%d bytes]\n",
                  ints.length, path, Files.size(path));

运行此代码将显示以下内容。

Wrote 6 ints to files\ints.bin [24 bytes]

由于每个 int 占 4 个字节,因此文件大小为 24 个字节,如控制台所示。

读取基本类型

DataInputStream 从二进制流中读取基本类型。它装饰了一个 InputStream,您必须提供它来构建任何 DataInputStream 实例。这个新实例将所有读取操作委托给您提供的 InputStream

它提供以下方法,这些方法是不言自明的。每个方法都返回相应的类型。

它提供方法来读取无符号字节和短整型

  • readUnsignedByte():读取一个无符号字节,并以 int 形式返回,范围为 0 到 255。
  • readUnsignedShort():读取两个字节,并将它们解码为无符号 16 位整数。该值以 int 形式返回,范围为 0 到 65535。

它还提供方法来读取多个字节并将它们排列成一个字符字符串。

以下代码可以用来读取您在前面示例中创建的文件中写入的整数。

Path path = Path.of("files/ints.bin");
int[] ints = new int[6];
try (var inputStream = Files.newInputStream(path);
     var dataInputStream = new DataInputStream(inputStream);) {

    for (int index = 0; index < ints.length; index++) {
        ints[index] = dataInputStream.readInt();
    }

    System.out.println("ints = " + Arrays.toString(ints));

} catch (IOException e) {
    e.printStackTrace();
}

上次更新: 2023 年 1 月 25 日


系列中的上一篇
当前教程
装饰 IO 流
系列中的下一篇

系列中的上一篇: 读取和写入二进制文件

系列中的下一篇: 内存中的 IO 流