Java BufferedInputStream
介绍
在 Java I/O 操作中,BufferedInputStream 是一个非常重要的类,它为输入流提供了缓冲功能,可以显著提高读取数据的效率。简单来说,BufferedInputStream 在内存中创建一个缓冲区,当我们读取数据时,先从这个缓冲区读取,只有当缓冲区数据不足时才会从底层输入流中读取数据,这大大减少了 I/O 操作的次数。
BufferedInputStream 属于 Java I/O 包中的装饰器模式的一个实现,它可以包装任何 InputStream 对象,为其添加缓冲功能。
工作原理
BufferedInputStream 的工作原理可以用下面的图表来表示:
当应用程序请求读取数据时:
- 首先检查缓冲区是否有足够的数据
- 如果有,直接从缓冲区返回数据
- 如果没有,从底层输入流中读取一大块数据到缓冲区,然后返回请求的数据
BufferedInputStream 类的基本用法
创建 BufferedInputStream 对象
要创建一个 BufferedInputStream 对象,我们需要首先有一个 InputStream 对象:
// 创建一个基本的 FileInputStream
FileInputStream fileInputStream = new FileInputStream("example.txt");
// 包装为 BufferedInputStream
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
我们还可以指定缓冲区的大小:
// 使用 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 读取文件的简单示例:
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!
使用字节数组读取
使用字节数组可以一次读取多个字节,进一步提高效率:
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 支持标记和重置功能,这在某些场景下非常有用:
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 包装后读取大文件的性能:
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 可以提高这类操作的效率:
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:图片处理
在处理图像数据时,使用缓冲可以显著提高效率:
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 操作的次数:
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();
}
}
最佳实践
-
始终使用 try-with-resources 语句:确保流在使用后被正确关闭,避免资源泄露。
-
适当选择缓冲区大小:默认的 8KB 缓冲区对大多数应用来说已经足够,但对于特定场景可以调整:
- 小文件或频繁小量读取:可以使用较小的缓冲区
- 大文件或流媒体:可以使用较大的缓冲区
-
结合 BufferedReader:对于文本文件,可以将 BufferedInputStream 与 BufferedReader 结合使用,获得更好的性能和更便捷的行读取功能。
-
不要重复缓冲:不要将已经缓冲的流再次包装在缓冲流中,这会浪费内存。
总结
BufferedInputStream 是 Java I/O 操作中的重要工具,它通过内存缓冲机制显著提高了数据读取的效率,特别是在处理大文件或频繁的小量数据读取时。其主要优点包括:
- 减少底层 I/O 操作次数
- 提高数据读取速度
- 支持标记和重置功能
在实际应用中,几乎所有需要从输入流读取数据的场景都应该考虑使用 BufferedInputStream,它是 Java I/O 操作性能优化的基本手段之一。
练习
-
创建一个程序,使用 BufferedInputStream 读取一个文本文件,统计其中的字符数、单词数和行数。
-
修改上述程序,比较使用 FileInputStream 和 BufferedInputStream 的性能差异。
-
实现一个简单的文件复制程序,使用 BufferedInputStream 和 BufferedOutputStream 以提高性能。
-
使用 mark() 和 reset() 方法实现一个预览功能,当文件开头包含特定的魔数时才继续处理该文件。