本文纲要
- 字节流拷贝的回顾与痛点
- 字节缓冲流概述
- 缓冲流源码分析:缓冲区大小与自动关闭
- 缓冲流一次读写一个字节的实现与原理
4.1 代码实现
4.2 底层原理与内存模型 - 缓冲流结合字节数组的高效拷贝
5.1 代码实现
5.2 与单字节方式的区别 - 四种拷贝方式对比总结
- 项目代码结构
- 完整代码示例
- 小结
字节流拷贝的回顾与痛点
在学习 Java IO 的初期,我们通常使用 FileInputStream 和 FileOutputStream 进行文件拷贝。常见的两种基础方式为:
- 一次读写一个字节:每次从源文件读取一个字节,再写入目标文件。这种方式虽然简单,但每次读写都需要与硬盘交互,性能极低。
- 一次读写一个字节数组:手动创建一个
byte[]数组(例如new byte[1024]),每次读取1024字节,再写入目标文件。这种方式减少了与硬盘的交互次数,性能大幅提升。
Java 的设计者显然意识到了手动管理数组的麻烦,因此在标准库中提供了字节缓冲流,内置了缓冲区,让开发者无需手动创建数组即可获得高效的文件读写能力。
字节缓冲流概述
Java 中的字节缓冲流分为两种:
BufferedInputStream:字节缓冲输入流,用于高效读取数据。BufferedOutputStream:字节缓冲输出流,用于高效写出数据。
核心特点:
- 缓冲流本身不直接操作文件,必须依赖底层的字节流(如
FileInputStream/FileOutputStream)才能真正读写数据。 - 缓冲流的本质是在内存中提供了一个默认长度为
8192字节的数组,用于暂存数据,从而减少与硬盘的交互次数,大幅提升效率。 - 当关闭缓冲流时,其关联的底层字节流也会被自动关闭。
缓冲流源码分析:缓冲区大小与自动关闭
通过查看 JDK 源码,我们可以验证缓冲区的大小和关闭行为。
1 ) 缓冲区大小
在 BufferedInputStream 的构造方法中,会调用另一个构造方法,内部使用 DEFAULTBUFFERSIZE 常量:
// BufferedInputStream 源码片段
private static int DEFAULTBUFFERSIZE = 8192;
public BufferedInputStream(InputStream in) {
this(in, DEFAULTBUFFERSIZE);
}
在底层构造中,会创建一个长度为 size 的字节数组:
byte[] buf = new byte[size];
同样,BufferedOutputStream 的构造方法也遵循相同逻辑,底层创建长度为 8192 的字节数组。
2 ) 自动关闭底层流
在 close() 方法中,缓冲流会关闭其包装的底层字节流:
// BufferedInputStream 的 close() 方法
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close();
return;
}
}
}
因此,我们只需关闭缓冲流对象,底层的 FileInputStream / FileOutputStream 也会随之关闭,无需单独处理。
缓冲流一次读写一个字节的实现与原理
1 ) 代码实现
用缓冲流以单字节方式拷贝文件,代码极为简洁,与基础字节流的写法几乎一致,只是将 FileInputStream/FileOutputStream 包装在缓冲流中。
public class OutputDemo11 {
public static void main(String[] args) throws IOException {
// 创建字节缓冲输入流,底层自动创建长度为8192的字节数组
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("bytestream\\a.avi"));
// 创建字节缓冲输出流,底层自动创建长度为8192的字节数组
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("bytestream\\copy.avi"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
// 关闭缓冲流,底层字节流也会被关闭
bis.close();
bos.close();
}
}
2 ) 底层原理与内存模型
尽管这里每次还是读写一个字节,但缓冲流内部有一个 8192 字节的数组作为缓冲区,其工作流程如下:
关键过程:
- bis.read() 底层会一次性从硬盘读取 8192 字节,填充到缓冲输入流的内部数组中。
- 后续调用 read() 时,实际是从内存中的数组逐字节读取,速度极快。
- bos.write(b) 先将字节写入缓冲输出流的内部数组,当数组满 8192 字节时,再一次性地将整个数组写入硬盘。
- 反复循环,直到文件拷贝完毕。
优势:将多次与硬盘的交互合并为少数几次大块传输,大幅减少内存与硬盘之间的数据交换次数,从而提升性能。
缓冲流结合字节数组的高效拷贝
1 ) 代码实现
在缓冲流的基础上,再配合自定义的字节数组,可以进一步减少 read() 和 write() 方法的调用次数,性能更优。
public class OutputDemo12 {
public static void main(String[] args) throws IOException {
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("bytestream\\a.avi"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("bytestream\\copy.avi"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bis.close();
bos.close();
}
}
2 ) 与单字节方式的区别
此时的 read(byte[]) 和 write(byte[], int, int) 是以“块”为单位进行读写,流程如下:
区别:
- 在单字节方式中,内存中的“倒手”是一字节一字节进行的,循环次数多。
- 在数组方式中,每次可以倒手
1024(或用户自定义长度)个字节,循环次数大幅减少,效率更高。
四种拷贝方式对比总结
| 拷贝方式 | 使用的流 | 一次读写单位 | 优点 | 缺点 |
|---|---|---|---|---|
| 字节流 + 单字节 | FileInputStream / FileOutputStream | 1 字节 | 代码简单 | 极慢,每次读写都访问硬盘 |
| 字节流 + 字节数组 | FileInputStream / FileOutputStream | 自定义数组长度 | 速度较快,减少硬盘交互次数 | 需手动管理数组 |
| 缓冲流 + 单字节 | BufferedInputStream / BufferedOutputStream | 底层 8192 字节 | 无需手动创建数组,速度介于两者之间 | 内存倒手次数多 |
| 缓冲流 + 字节数组 | BufferedInputStream / BufferedOutputStream | 自定义数组长度 + 底层 8192 字节 | 速度最快,开发效率高 | 代码稍复杂 |
项目代码结构
本示例的项目结构如下:
bytestream/
└── src/
└── com/
└── wb/
└── output/
├── OutputDemo11.java // 缓冲流 - 一次读写一个字节
└── OutputDemo12.java // 缓冲流 - 一次读写一个字节数组
完整代码示例
OutputDemo11.java
package com.wb.output;
import java.io.*;
public class OutputDemo11 {
public static void main(String[] args) throws IOException {
// 利用缓冲流拷贝文件(一次读写一个字节)
// 创建一个字节缓冲输入流
// 在底层创建了一个默认长度为8192的字节数组
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("bytestream\\a.avi"));
// 创建一个字节缓冲输出流
// 在底层也创建了一个默认长度为8192的字节数组
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("bytestream\\copy.avi"));
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
// 方法的底层会把字节流也给关闭
bis.close();
bos.close();
}
}
OutputDemo12.java
package com.wb.output;
import java.io.*;
public class OutputDemo12 {
public static void main(String[] args) throws IOException {
// 缓冲流结合数组,进行文件拷贝
// 创建一个字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("bytestream\\a.avi"));
// 创建一个字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("bytestream\\copy.avi"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bis.close();
bos.close();
}
}
小结
字节流可以操作所有类型的文件(图片、音频、视频等),但直接使用效率较低
字节缓冲流通过内置的 8192 字节数组,减少了硬盘与内存的交互次数,显著提升读写效率
缓冲流不能直接操作文件,必须包装一个基础字节流,真正干活的是底层字节流
根据需求,可选择单字节或字节数组的读写方式,其中缓冲流 + 字节数组是推荐的最高效拷贝方式
关闭缓冲流时,底层字节流会被自动关闭,无需重复关闭
掌握字节缓冲流,是 Java IO 进阶的重要一步,也是后续学习字符流、对象流等高级 IO 的基础
1201

被折叠的 条评论
为什么被折叠?



