跳到主要内容

Java BufferedInputStream

介绍

在 Java I/O 操作中,BufferedInputStream 是一个非常重要的类,它为输入流提供了缓冲功能,可以显著提高读取数据的效率。简单来说,BufferedInputStream 在内存中创建一个缓冲区,当我们读取数据时,先从这个缓冲区读取,只有当缓冲区数据不足时才会从底层输入流中读取数据,这大大减少了 I/O 操作的次数。

BufferedInputStream 属于 Java I/O 包中的装饰器模式的一个实现,它可以包装任何 InputStream 对象,为其添加缓冲功能。

工作原理

BufferedInputStream 的工作原理可以用下面的图表来表示:

当应用程序请求读取数据时:

  1. 首先检查缓冲区是否有足够的数据
  2. 如果有,直接从缓冲区返回数据
  3. 如果没有,从底层输入流中读取一大块数据到缓冲区,然后返回请求的数据

BufferedInputStream 类的基本用法

创建 BufferedInputStream 对象

要创建一个 BufferedInputStream 对象,我们需要首先有一个 InputStream 对象:

java
// 创建一个基本的 FileInputStream
FileInputStream fileInputStream = new FileInputStream("example.txt");

// 包装为 BufferedInputStream
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

我们还可以指定缓冲区的大小:

java
// 使用 8192 字节的缓冲区
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream, 8192);

如果不指定缓冲区大小,默认使用 8192 字节(8KB)的缓冲区。

常用方法

BufferedInputStream 继承自 InputStream,因此拥有 InputStream 的所有方法,最常用的方法包括:

  • int read(): 读取单个字节
  • int read(byte[] b, int off, int len): 读取多个字节到数组中
  • void close(): 关闭流
  • int available(): 返回可以读取的字节数
  • boolean markSupported(): 检查是否支持标记
  • void mark(int readlimit): 标记当前位置
  • void reset(): 重置到最近的标记位置

示例代码

基本读取操作

下面是一个使用 BufferedInputStream 读取文件的简单示例:

java
import java.io.*;

public class BufferedInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {

int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

如果 example.txt 文件内容为 "Hello, BufferedInputStream!",则输出为:

Hello, BufferedInputStream!

使用字节数组读取

使用字节数组可以一次读取多个字节,进一步提高效率:

java
import java.io.*;

public class BufferedInputStreamArrayExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {

byte[] buffer = new byte[1024];
int bytesRead;

while ((bytesRead = bis.read(buffer)) != -1) {
// 将字节数组转换为字符串
String chunk = new String(buffer, 0, bytesRead);
System.out.print(chunk);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用 mark() 和 reset() 方法

BufferedInputStream 支持标记和重置功能,这在某些场景下非常有用:

java
import java.io.*;

public class BufferedInputStreamMarkResetExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("example.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {

// 读取前5个字符
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}
System.out.println(); // 换行

// 标记当前位置
bis.mark(100); // 参数表示在此位置之后最多可以读取的字节数

// 继续读取5个字符
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}
System.out.println(); // 换行

// 重置到标记位置
bis.reset();

// 再次读取5个字符,应该与上面相同
for (int i = 0; i < 5; i++) {
System.out.print((char) bis.read());
}

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

如果文件内容为 "Hello, BufferedInputStream!",则输出为:

Hello
, Buf
, Buf

BufferedInputStream 与 FileInputStream 的性能比较

为了展示 BufferedInputStream 的性能优势,我们可以比较直接使用 FileInputStream 和使用 BufferedInputStream 包装后读取大文件的性能:

java
import java.io.*;

public class PerformanceComparisonExample {
public static void main(String[] args) {
// 假设有一个较大的文件 "largefile.txt"
String filename = "largefile.txt";

// 使用 FileInputStream 直接读取
long startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(filename)) {
int data;
while ((data = fis.read()) != -1) {
// 只读取,不做任何处理
}
} catch (IOException e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("FileInputStream 读取时间: " + (endTime - startTime) + " ms");

// 使用 BufferedInputStream 读取
startTime = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(filename);
BufferedInputStream bis = new BufferedInputStream(fis)) {
int data;
while ((data = bis.read()) != -1) {
// 只读取,不做任何处理
}
} catch (IOException e) {
e.printStackTrace();
}
endTime = System.currentTimeMillis();
System.out.println("BufferedInputStream 读取时间: " + (endTime - startTime) + " ms");
}
}
备注

对于大文件,BufferedInputStream 通常会显示出明显的性能优势,尤其是在使用单字节读取方法 read() 时。差异可能是数十倍甚至上百倍。

实际应用场景

场景1:读取配置文件

在许多应用程序中,需要读取配置文件,BufferedInputStream 可以提高这类操作的效率:

java
public class ConfigReader {
public static Properties loadConfig(String filename) {
Properties properties = new Properties();

try (FileInputStream fis = new FileInputStream(filename);
BufferedInputStream bis = new BufferedInputStream(fis)) {

properties.load(bis);

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

return properties;
}
}

场景2:图片处理

在处理图像数据时,使用缓冲可以显著提高效率:

java
public class ImageProcessor {
public static byte[] readImageData(String imageFile) {
try {
File file = new File(imageFile);
byte[] buffer = new byte[(int) file.length()];

try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis)) {

bis.read(buffer);
}

return buffer;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

场景3:网络数据传输

当从网络读取数据时,使用 BufferedInputStream 可以减少网络 I/O 操作的次数:

java
import java.io.*;
import java.net.*;

public class NetworkReader {
public static String readFromUrl(String urlString) {
StringBuilder content = new StringBuilder();

try {
URL url = new URL(urlString);
URLConnection connection = url.openConnection();

try (BufferedInputStream bis = new BufferedInputStream(connection.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(bis))) {

String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
} catch (IOException e) {
e.printStackTrace();
}

return content.toString();
}
}

最佳实践

  1. 始终使用 try-with-resources 语句:确保流在使用后被正确关闭,避免资源泄露。

  2. 适当选择缓冲区大小:默认的 8KB 缓冲区对大多数应用来说已经足够,但对于特定场景可以调整:

    • 小文件或频繁小量读取:可以使用较小的缓冲区
    • 大文件或流媒体:可以使用较大的缓冲区
  3. 结合 BufferedReader:对于文本文件,可以将 BufferedInputStream 与 BufferedReader 结合使用,获得更好的性能和更便捷的行读取功能。

  4. 不要重复缓冲:不要将已经缓冲的流再次包装在缓冲流中,这会浪费内存。

总结

BufferedInputStream 是 Java I/O 操作中的重要工具,它通过内存缓冲机制显著提高了数据读取的效率,特别是在处理大文件或频繁的小量数据读取时。其主要优点包括:

  • 减少底层 I/O 操作次数
  • 提高数据读取速度
  • 支持标记和重置功能

在实际应用中,几乎所有需要从输入流读取数据的场景都应该考虑使用 BufferedInputStream,它是 Java I/O 操作性能优化的基本手段之一。

练习

  1. 创建一个程序,使用 BufferedInputStream 读取一个文本文件,统计其中的字符数、单词数和行数。

  2. 修改上述程序,比较使用 FileInputStream 和 BufferedInputStream 的性能差异。

  3. 实现一个简单的文件复制程序,使用 BufferedInputStream 和 BufferedOutputStream 以提高性能。

  4. 使用 mark() 和 reset() 方法实现一个预览功能,当文件开头包含特定的魔数时才继续处理该文件。

附加资源