【问题标题】:Buffer chaining and decorative principle Java I/O缓冲区链接和装饰原理 Java I/O
【发布时间】:2020-07-17 19:02:29
【问题描述】:

是的,我知道缓冲区是什么。但请注意:

BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("file.txt"));

缓冲实际上是如何工作的?在我看来,我们在 FileWriter 缓冲区而不是 BufferedWriter 缓冲区中缓冲数据。因为当 BufferedWriter 的缓冲区满了时,它会发送到 FileWriter 缓冲区并负责写入数据?

我错过了什么吗?我看到它的方式:看起来我们正在从一个更大的容器中吸取水到一个较小的容器中。所以我们最终从较小的一个倒水。

类似的例子在这里:

Scanner scanner = new BufferedReader(new FileReader("file.txt"));
scanner.nextLine();

我到处都看到过这种情况。实际上,我们最终会逐行读取 Scanner 而不是缓冲区及其 8k 容量。那么这里的缓冲区有什么意义呢?我们从文件中逐行读取,而不是一次读取整个缓冲区。这里bufferedReader是多余的吗?

如果有人可以很好地解释这一点,我已经挣扎了很长时间。

【问题讨论】:

    标签: java io buffer


    【解决方案1】:

    对读取和写入数据的低级系统调用进行了优化,可以一次传输更大的块。缓冲让您可以利用这一点。当你写单个字符或短字符串时,它们都被累积在一个缓冲区中,当缓冲区已满时,它们作为一个大块写出。当您读取数据时,读取函数会请求填充一个大缓冲区,然后从该缓冲区返回数据。

    您说得对,将缓冲流包装在其他缓冲流中是没有意义的:充其量它什么也做不了,最坏的情况是它增加了开销,因为数据不必要地从一个缓冲区复制到下一个缓冲区。离数据源最近的缓冲区最重要。

    另一方面,API 规范中没有规定 FileWriter 和 FileReader 有缓冲区。事实上,它推荐wrap FileWriter within a BufferedWriterFileReader within a BufferedReader

    为了获得最高效率,请考虑将OutputStreamWriter 包装在BufferedWriter 中,以避免频繁的转换器调用。例如:

    Writer out
      = new BufferedWriter(new OutputStreamWriter(System.out));
    

    (FileWriter 是 OutputStreamWriter 的子类)

    这在内部是如何工作的?

    如果你看一下 FileWriter 是如何实现的,故事会变得复杂,因为 FileWriter 确实 涉及缓冲区。一些细节可能取决于您使用的 Java 版本。在 OpenJDK 中,当您创建一个装饰 FileWriter 的 BufferedWriter 时:

    BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("file.txt"));
    

    您正在创建一堆对象,如下所示,其中一个对象包装下一个对象:

    BufferedWriter -> FileWriter -> StreamEncoder -> FileOutputStream
    

    其中StreamEncoder 是一个内部类,是OutputStreamWriter 实现方式的一部分。

    现在,当您将字符写入BufferedWriter 实例时,它首先将它们累积到 BufferedWriter 自己的缓冲区中。内部FileWriter 不会看到任何数据,直到您写入足够的数据来填充此缓冲区(或调用flush())。

    BufferedWriter 缓冲区已满时,它会通过一次调用write(char[],int,int) 将缓冲区的内容写入FileWriter。这种大数据块的传输是效率的来源:现在 FileWriter 有大块数据可以写入文件,而不是单个字符。

    然后有点复杂:必须将字符转换为字节,以便将它们写入文件。这是 FileWriter 将这些数据传递给 StreamEncoder 的地方。

    StreamEncoder 类使用CharsetEncoder 将字符块一次性转换为字节,并将字节累积到自己的缓冲区中。完成后,它将字节作为一个块写入最里面的 FileOutputStream。 FileOutputStream 然后调用操作系统函数来写入一个实际的文件。

    如果不使用 BufferedWriter 会怎样?

    如果您直接将字符写入 FileWriter,它们将被传递到 StreamEncoder 对象,该对象将它们转换为字节并存储在其私有缓冲区中,而不是直接写入 FileOutputStream。这样,FileWriter 的内部实现为您提供了一些缓冲的好处。但这不是 API 规范的一部分,因此您不应该依赖它。

    此外,每次调用 FileWriter.write 都会调用 CharsetEncoder 以将字符编码为字节。一次对大块字符进行编码效率更高,编写单个字符或短字符串的开销更高。

    【讨论】:

    • 谢谢,这是一个很好的答案。我不同意/不理解的一件事是 FileWriter 没有缓冲。如果你写了一些东西而不刷新它,它永远不会写(除非它的缓冲区已满 -1kb 我认为)。还有一件事:假设 FileWriter 确实有 1kb,而 bufferedWriter 有 8kb。如果我要写入 10kb 的数据,Java 会发出 2 个 I/O 请求(查看更大的缓冲区)还是 10 个 I/O 请求(查看底层缓冲区)?
    • FileWriter 没有自己的缓冲区 - 我添加了指向源代码的链接,以便您自己查看。 FileWriter 将数据写入一个 StreamEncoder,它有一个 8192 字节的缓冲区。如果 StreamEncoder 缓冲区为 1k,而您将 10k 写入文件 FileWriter,您将看到 10 次 1k 的写入操作。
    • 非常感谢!就一件事。如果我读错了,请纠正我。所以有一个缓冲区在转换之前缓冲,还有另一个缓冲区在写入之前缓冲,对吗?如果我当然使用 FileWriter..
    • 直接写入 FileWriter 时只有一个缓冲区:字节缓冲区,将字符编码结果累积为字节。当该缓冲区变满时,它会被写入输出文件
    • 每次将字符编码为字节的调用都有一些开销,而批量编码的总开销低于 1×1。默认的 BufferedWriter 缓冲区为 8k chars,而 StreamEncoder 缓冲区为 8k bytes。在 ASCII 文本字符映射到字节 1 到 1 等重要边缘情况下,编码 8k 字符会产生 8k 字节,因此 StreamEncoder 只需调用一次 CharsetEncoder。在一般情况下,一个字符可能被编码为几个字节,并且编码 8k 字符的结果不能一次全部放入 StreamEncoder 缓冲区。在这种情况下,StreamEncoder 必须调用 CharsetEncoder 两次。
    猜你喜欢
    • 2015-08-20
    • 1970-01-01
    • 2015-07-05
    • 1970-01-01
    • 2014-09-20
    • 2012-01-14
    • 1970-01-01
    • 2011-10-05
    • 1970-01-01
    相关资源
    最近更新 更多