在 Java 中,BufferedInputStream 类是 Java 输入流体系里的一个装饰器类,它的作用是为输入流提供缓冲功能,以此提升输入操作的效率。
BufferedInputStream 类为 InputStream 提供透明的字节块读取和缓冲功能,读取较大的字节块并对其进行缓冲可以大大加快 IO 的速度。
缓冲输入流(BufferedInputStream)不是一次从网络或磁盘读取一个字节,而是一次将较大的数据块读入内部缓冲区。因此,当您从 BufferedInputStream 中读取一个字节时,就是从其内部缓冲区中读取的,当缓冲区中的数据被读取完毕后,BufferedInputStream 会将另一个较大的数据块读入缓冲区。如下图:
📢注意,这通常比从 InputStream 一次读取一个字节要快得多,尤其是在磁盘访问和数据量较大的情况下。
如果要为 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 的最佳缓冲区大小,你可以先了解硬盘读取的数据块大小,也可能需要考虑其缓存大小,然后将缓冲区大小设置为该数值的倍数。当然,要精准找到最佳缓冲区大小,实验是必不可少的环节。你可以通过测量不同缓冲区大小下的读取速度,来评估和比较性能,从而做出最优选择。
值得注意的是,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
完成从 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 被关闭。