我应该缓冲InputStream还是InputStreamReader?

29

以下两种缓冲方式有何不同(如果有的话)?

Reader r1 = new BufferedReader(new InputStreamReader(in, "UTF-8"), bufferSize);
Reader r2 = new InputStreamReader(new BufferedInputStream(in, bufferSize), "UTF-8");
4个回答

34

r1更高效。 InputStreamReader本身没有很大的缓冲区。 BufferedReader可以设置比InputStreamReader更大的缓冲区。 在r2中的InputStreamReader将充当瓶颈。

简而言之:您应该通过漏斗读取数据,而不是通过瓶子读取。


更新:这里有一个小基准测试程序,只需复制粘贴即可运行。 您无需准备文件。

package com.stackoverflow.q3459127;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;

public class Test {

    public static void main(String... args) throws Exception {

        // Init.
        int bufferSize = 10240; // 10KB.
        int fileSize = 100 * 1024 * 1024; // 100MB.
        File file = new File("/temp.txt");

        // Create file (it's also a good JVM warmup).
        System.out.print("Creating file .. ");
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(file));
            for (int i = 0; i < fileSize; i++) {
                writer.write("0");
            }
            System.out.printf("finished, file size: %d MB.%n", file.length() / 1024 / 1024);
        } finally {
            if (writer != null) try { writer.close(); } catch (IOException ignore) {}
        }

        // Read through funnel.
        System.out.print("Reading through funnel .. ");
        Reader r1 = null;        
        try {
            r1 = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"), bufferSize);
            long st = System.nanoTime();
            for (int data; (data = r1.read()) > -1;);
            long et = System.nanoTime();
            System.out.printf("finished in %d ms.%n", (et - st) / 1000000);
        } finally {
            if (r1 != null) try { r1.close(); } catch (IOException ignore) {}
        }

        // Read through bottle.
        System.out.print("Reading through bottle .. ");
        Reader r2 = null;        
        try {
            r2 = new InputStreamReader(new BufferedInputStream(new FileInputStream(file), bufferSize), "UTF-8");
            long st = System.nanoTime();
            for (int data; (data = r2.read()) > -1;);
            long et = System.nanoTime();
            System.out.printf("finished in %d ms.%n", (et - st) / 1000000);
        } finally {
            if (r2 != null) try { r2.close(); } catch (IOException ignore) {}
        }

        // Cleanup.
        if (!file.delete()) System.err.printf("Oops, failed to delete %s. Cleanup yourself.%n", file.getAbsolutePath());
    }

}

我的Latitude E5500电脑使用Seagate Momentus 7200.3硬盘的测试结果:

创建文件..已完成,文件大小:99 MB。
通过漏斗读取..在1593毫秒内完成。
通过瓶子读取..在7760毫秒内完成。

如果底层的InputStream是FileInputStream,那么在整个读取过程中这两个读取器执行的磁盘读取量会有所不同吗? - bdkosher
我使用perfmon进行了检查,没有看到明显的差异。我很快会更新答案,包括一个基准代码片段。 - BalusC
2
包名很棒,点个赞 :) - AlikElzin-kilaka
为什么不将磁盘读取也缓冲起来呢?如果不这样做,难道不是意味着 inputStream 必须对源码每个字节调用 read 函数吗?我看不出 BDKosher 对于磁盘读取的担忧是如何没有被证明的,似乎有一个缓冲的 InputStream 应该会减少磁盘读取次数。BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputSream(inputStream), "UTF-8")); - ross studtman

5

r1 更为方便,特别是在您以行为基础的流作为读取源时,因为 BufferedReader 支持 readLine 方法。您不必将内容读入字符数组缓冲区或逐个字符读取。但是,您需要将 r1 强制转换为 BufferedReader 或显式地为变量使用该类型。

我经常使用这段代码片段:

BufferedReader br = ...
String line;
while((line=br.readLine())!=null) {
  //process line
}

2

回复Ross Studtman在上面的评论中提出的问题(但也与OP相关):

BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputSream(inputStream), "UTF-8"));
BufferedInputStream是多余的(并且可能会因为额外的复制而损害性能)。这是因为BufferedReader通过调用InputStreamReader.read(char[], int, int)以大块方式请求来自InputStreamReader的字符,后者通过StreamDecoder再次调用InputStream.read(byte[], int, int)从底层的InputStream中读取大块字节。

您可以通过运行以下代码来验证这一点:

new BufferedReader(new InputStreamReader(new ByteArrayInputStream("Hello world!".getBytes("UTF-8")) {

    @Override
    public synchronized int read() {
        System.err.println("ByteArrayInputStream.read()");
        return super.read();
    }

    @Override
    public synchronized int read(byte[] b, int off, int len) {
        System.err.println("ByteArrayInputStream.read(..., " + off + ", " + len + ')');
        return super.read(b, off, len);
    }

}, "UTF-8") {

    @Override
    public int read() throws IOException {
        System.err.println("InputStreamReader.read()");
        return super.read();
    }

    @Override
    public int read(char[] cbuf, int offset, int length) throws IOException {
        System.err.println("InputStreamReader.read(..., " + offset + ", " + length + ')');
        return super.read(cbuf, offset, length);
    }

}).read(); // read one character from the BufferedReader

您将看到以下输出:
InputStreamReader.read(..., 0, 8192)
ByteArrayInputStream.read(..., 0, 8192)

这说明 BufferedReaderInputStreamReader 请求了一大块字符,而 InputStreamReader 又从底层的 InputStream 请求了一大块字节。

如果您使用BufferedInputStream,它会以大块请求数据从InputStream中获取,并从其缓冲区提供Readers的较小请求。这并不是“多余的”。 - user207421
@EJP:在我的示例片段中(答案中的第一个代码块),BufferedInputStream 是多余的,因为 BufferedReaderInputStreamReader 请求大块数据,而 InputStreamReader 又从底层的 InputStream 请求大块数据。在 InputStreamReader 和底层 InputStream 之间插入 BufferedInputStream 只会增加开销,而不会带来任何性能提升。 - Matt Whitlock

1

如果您在Java 8中打开一个文件,可以使用Files.newBufferedReader(Path)。我不知道这种方法的性能如何与其他解决方案相比,但至少它将缓冲什么构造推入了JDK。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接