Java IO:BufferedInputStream 类

在 Java 中,BufferedInputStream 类是 Java 输入流体系里的一个装饰器类,它的作用是为输入流提供缓冲功能,以此提升输入操作的效率。

BufferedInputStream 类为 InputStream 提供透明的字节块读取和缓冲功能,读取较大的字节块并对其进行缓冲可以大大加快 IO 的速度。

缓冲输入流(BufferedInputStream)不是一次从网络或磁盘读取一个字节,而是一次将较大的数据块读入内部缓冲区。因此,当您从 BufferedInputStream 中读取一个字节时,就是从其内部缓冲区中读取的,当缓冲区中的数据被读取完毕后,BufferedInputStream 会将另一个较大的数据块读入缓冲区。如下图:

image.png

📢注意,这通常比从 InputStream 一次读取一个字节要快得多,尤其是在磁盘访问和数据量较大的情况下。

BufferedInputStream 示例

如果要为 InputStream 添加缓冲功能,只需将其封装在 BufferedInputStream 中即可。如下所示:

BufferedInputStream bufferedInputStream = new BufferedInputStream(
                      new FileInputStream("D:\\input-file.txt"));

如上述代码,使用 BufferedInputStream 为无缓冲功能的 InputStream 类添加缓冲功能非常简单,直接将 InputStream 类传递给 BufferedInputStream 类的构造方法。

BufferedInputStream 在内部创建了一个字节数组,并试图通过调用底层 InputStream 上的 InputStream.read(byte[]) 方法来填充该数组。

部分源码:

// 内部缓冲区,存放获取到字节块
protected volatile byte[] buf; 
// 缓冲区中最后一个有效字节的索引加一,即缓冲区中字节个数
protected int count;
// 缓冲区中的当前位置。这是从 buf 数组中读取下一个字符的索引。
protected int pos;

// 一次读取一个字节
public int read() throws IOException {
    if (lock != null) {
        lock.lock();
        try {
            return implRead(); // 重点,实现读取
        } finally {
            lock.unlock();
        }
    } else {
        synchronized (this) {
            return implRead();
        }
    }
}

// 读取一个字节的具体实现
private int implRead() throws IOException {
    if (pos >= count) { // 判断读取字节的位置和缓存中字节数个数
        fill(); // 填充
        if (pos >= count)
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}

// 字节填充
private void fill() throws IOException {
    byte[] buffer = getBufIfOpen();
    //...
    count = pos;
    // 这里从底层 InputStream 流中读取 buffer.length - pos 个字节
    int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
    if (n > 0)
        count = n + pos;
}

private InputStream getInIfOpen() throws IOException {
    InputStream input = in;
    if (input == null)
        throw new IOException("Stream closed");
    return input;
}

设置缓冲区大小

你可以设置 BufferedInputStream 内部使用的缓冲区大小,可以将缓冲区大小作为参数提供给 BufferedInputStream 构造函数,如下所示:

BufferedInputStream bufferedInputStream = new BufferedInputStream(
                new FileInputStream("c:\\data\\input-file.txt"), 2048);

上面将 BufferedInputStream 使用的内部缓冲区设置为 2KB。

📢注意:最好使用 1024 字节倍数的缓冲区大小,这与硬盘等大多数内置缓冲区的效果最佳。它除了为输入流添加缓冲外,BufferedInputStream 的行为与 InputStream 完全相同。

如何设置最佳缓冲区大小?

你可以通过开展不同缓冲区大小的实验,来确定在你当前的硬件环境下,哪种缓冲区大小能实现最优性能。最佳的缓冲区大小选择,很大程度上取决于你是将 BufferedInputStream 应用于磁盘输入流,还是网络输入流。

无论是处理磁盘流还是网络流,具体计算机的硬件配置都会对最佳缓冲区大小产生影响。例如,若硬盘的最小读取数据量为 4KB,那么使用小于 4KB 的缓冲区就不太合适,建议优先选择 4KB 倍数的缓冲区大小,像 6KB 这样的非 4KB 倍数的缓冲区就不是理想之选。

即便磁盘每次读取的数据块仅为 4KB,使用更大的缓冲区依然是明智之举。硬盘在顺序读取数据方面表现出色,这意味着它能够高效地读取多个相邻的数据块。所以,在 BufferedInputStream 中采用 16KB 或 64KB(甚至更大)的缓冲区,相较于仅使用 4KB 缓冲区,往往能带来更优的性能表现。

此外,部分硬盘配备了高达数百万字节的读取缓存。当硬盘将 64KB 的文件读入内部缓存时,一次性将所有数据读入 BufferedInputStream 要比多次读取操作更为高效。多次读取不仅会降低读取速度,而且硬盘的读取缓存还有可能在两次读取操作的间隙被清空,进而导致硬盘不得不重新将数据块读入缓存。

若要确定 BufferedInputStream 的最佳缓冲区大小,你可以先了解硬盘读取的数据块大小,也可能需要考虑其缓存大小,然后将缓冲区大小设置为该数值的倍数。当然,要精准找到最佳缓冲区大小,实验是必不可少的环节。你可以通过测量不同缓冲区大小下的读取速度,来评估和比较性能,从而做出最优选择。

mark() 和 reset()

值得注意的是,BufferedInputStream 类支持从 InputStream 类继承而来的 mark() 和 reset() 方法。

📢 注意,并非所有 InputStream 子类都支持这些方法。一般来说,您可以调用 markSupported() 方法来了解给定的 InputStream 是否支持 mark() 和 reset(),但 BufferedInputStream 支持这些方法。

BufferedInputStream input = new BufferedInputStream(new FileInputStream("D:\\input.txt"));
System.out.println(input.markSupported()); // true

关闭 BufferedInputStream

完成从 Java BufferedInputStream 中读取数据后,必须将其关闭。您可以通过调用从 InputStream 继承而来的 close() 方法来关闭 BufferedInputStream。

关闭 Java BufferedInputStream 也将关闭 BufferedInputStream 正在读取和缓冲数据的 InputStream。下面是打开 BufferedInputStream、从中读取所有数据然后关闭它的示例:

BufferedInputStream bufferedInputStream = new BufferedInputStream(
                      new FileInputStream("D:\\input-file.txt"));
int data = bufferedInputStream.read();
while(data != -1) {
  data = bufferedInputStream.read(); // 读取数据
}
bufferedInputStream.close(); // 关闭

📢 注意,while 循环将持续从 BufferedInputStream 读取数据,直到 read() 方法返回 -1。while 循环退出,调用 BufferedInputStream close() 方法关闭流。

上述代码并不健壮,如果在从 BufferedInputStream 读取数据时出现异常,就永远不会调用 close() 方法。为了使代码更健壮,您必须使用 Java 的 try-with-resources 结构或者将 close() 操作放在 finally 中。

下面是一个使用 try-with-resources 结构关闭 BufferedInputStream 的示例:

try(BufferedInputStream bufferedInputStream =
        new BufferedInputStream( new FileInputStream("D:\\input-file.txt") ) ) {
    int data = bufferedInputStream.read();
    while(data != -1){
        data = bufferedInputStream.read();
    }
}

注意:BufferedInputStream 类是在 try 关键字后的括号内声明的。这向 Java 发出信号,表明该 BufferedInputStream 将由 try-with-resources 结构管理。

一旦执行线程退出 try 代码块,BufferedInputStream 就会关闭。如果在 try 代码块内部抛出异常,则会捕获异常,关闭 BufferedInputStream,然后重新抛出异常。因此,当在  try-with-resources 代码块中使用 BufferedInputStream 时,可以保证 BufferedInputStream 被关闭。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号