缓冲流 | JavaSE
缓冲流
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是缓冲流,以及缓冲流为什么能够改善 IO 读写性能。
- 能够区分
BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter四种缓冲流。 - 能够使用字节缓冲流高效复制文件。
- 能够使用
BufferedReader.readLine()按行读取文本。 - 能够使用
BufferedWriter.newLine()写出跨平台换行。 - 能够理解“节点流 + 包装流”的 IO 流组合思想。
- 能够判断什么时候应该使用字节缓冲流,什么时候应该使用字符缓冲流。
二、核心知识
2.1 为什么需要缓冲流
前面已经学习了:
FileInputStream
FileOutputStream
FileReader
FileWriter
它们都可以直接完成文件读写。
那么为什么还需要缓冲流?
核心原因是:
缓冲流通过在内存中设置缓冲区,减少底层 IO 操作的频率,从而改善数据读写性能。
可以先用一个生活中的例子理解。
假设需要搬 1000 本书。
第一种方式:
每次拿 1 本
书架 → 仓库
书架 → 仓库
书架 → 仓库
……
需要往返很多次。
第二种方式:
准备一个箱子
一次装几十本
箱子装满后一起搬走
搬运的数据没有变化,但是:
昂贵的“往返操作”减少了。
缓冲流的思想就是类似的。
2.2 Java 中四种核心缓冲流
Java IO 中主要有四种缓冲流:
| 类型 | 类 | 数据单位 | 作用 |
| -------------- | ---------------------- | -------- | ---------------------------- |
| 字节缓冲输入流 | BufferedInputStream | byte | 高效读取字节 |
| 字节缓冲输出流 | BufferedOutputStream | byte | 高效写出字节 |
| 字符缓冲输入流 | BufferedReader | char | 高效读取字符,并支持按行读取 |
| 字符缓冲输出流 | BufferedWriter | char | 高效写字符,并支持便捷换行 |
从整个 IO 体系来看:
字节流
├── InputStream
│ └── BufferedInputStream
│
└── OutputStream
└── BufferedOutputStream
字符流
├── Reader
│ └── BufferedReader
│
└── Writer
└── BufferedWriter
需要特别注意:
缓冲流通常不是直接取代底层流,而是包装已有 IO 流。
例如:
FileInputStream fis = new FileInputStream("a.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
可以理解为:
文件
↑
FileInputStream
↑
BufferedInputStream
↑
程序
程序不再直接操作 FileInputStream,而是通过 BufferedInputStream 进行读取。
这种:
一个流包装另一个流
的设计思想,在 Java IO 中非常重要。
2.3 什么是缓冲区
缓冲流内部维护一块内存区域,用于暂存数据。
教材中经常将默认缓冲区理解为:
8 KiB ≈ 8192 字节/字符规模
这个数字可以帮助理解缓冲思想,但编写业务代码时不应该依赖某个固定的默认缓冲区大小。
真正应该掌握的是:
缓冲流通过批量读取、批量写出降低底层 IO 调用频率。
2.4 缓冲流并不会改变数据类型
这是一个非常重要的判断题。
BufferedInputStream
包装以后是不是变成字符流了?
不是。
它仍然是:
字节输入流
同样:
BufferedReader
仍然是:
字符输入流
所以:
BufferedInputStream → byte
BufferedOutputStream → byte
BufferedReader → char
BufferedWriter → char
“Buffered”只表示:
增加了缓冲能力。
不是改变了数据类型。
三、使用方法
3.1 BufferedInputStream
BufferedInputStream 是:
字节缓冲输入流。
基本构造方式:
BufferedInputStream(InputStream in)
所以通常写成:
InputStream is =
new BufferedInputStream(
new FileInputStream("data.txt")
);
拆开来看就是:
FileInputStream fis = new FileInputStream("data.txt");
BufferedInputStream bis =
new BufferedInputStream(fis);
关系如下:
文件
↓
FileInputStream
↓
BufferedInputStream
↓
程序
3.2 BufferedOutputStream
BufferedOutputStream 是:
字节缓冲输出流。
构造方式:
BufferedOutputStream(OutputStream out)
典型代码:
OutputStream os =
new BufferedOutputStream(
new FileOutputStream("copy.txt")
);
结构:
程序
↓
BufferedOutputStream
↓
FileOutputStream
↓
文件
3.3 使用字节缓冲流复制文件
复制文件仍然属于:
读 → 写
只不过现在读写双方都加入了缓冲。
import java.io.*;
public class BufferedCopyDemo {
public static void main(String[] args) throws IOException {
try (
InputStream in =
new BufferedInputStream(
new FileInputStream("source.jpg")
);
OutputStream out =
new BufferedOutputStream(
new FileOutputStream("target.jpg")
)
) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
}
核心逻辑仍然只有:
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
所以一定不要产生这样的错误理解:
“有了 BufferedInputStream,就不需要 byte[] 了。”
两者解决的是不同层面的问题。
3.4 两层缓冲是什么关系
代码:
BufferedInputStream bis =
new BufferedInputStream(
new FileInputStream("a.mp4")
);
byte[] buffer = new byte[8192];
bis.read(buffer);
这里看起来存在两个 buffer:
BufferedInputStream 内部缓冲区
+
程序自己创建的 byte[]
它们并不冲突。
可以抽象成:
磁盘
↓
FileInputStream
↓
BufferedInputStream 内部缓冲
↓
byte[] 程序缓冲
↓
业务代码
BufferedInputStream 负责减少底层读取频率。
而:
byte[] buffer
则负责让你的程序:
一次处理一批数据,而不是每次只处理一个字节。
因此在传统阻塞式文件复制中:
BufferedInputStream
+
BufferedOutputStream
+
byte[]
是一种非常常见的组合。
3.5 BufferedReader
BufferedReader 是:
字符缓冲输入流。
构造器:
BufferedReader(Reader in)
例如:
Reader reader = new FileReader("article.txt");
BufferedReader br =
new BufferedReader(reader);
也可以直接写:
BufferedReader br =
new BufferedReader(
new FileReader("article.txt")
);
3.6 BufferedReader 最重要的方法:readLine()
BufferedReader 最值得掌握的新增能力是:
String readLine()
作用:
一次读取一行文本。
例如文件:
Java
MySQL
Spring Boot
Vue
代码:
import java.io.*;
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
try (
BufferedReader br =
new BufferedReader(
new FileReader("data.txt")
)
) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
核心模板:
String line;
while ((line = br.readLine()) != null) {
// 处理这一行
}
这是以后处理文本文件非常常见的代码。
3.7 readLine() 的返回值
假设文件:
hello
java
world
第一次:
br.readLine();
得到:
"hello"
第二次:
"java"
第三次:
"world"
继续读取:
null
因此:
while ((line = br.readLine()) != null)
的意思就是:
只要还能读取到一行,就继续处理。
3.8 readLine() 不包含换行符
这是一个很容易忽略的细节。
文件:
Java\n
MySQL\n
Redis
执行:
String line = br.readLine();
得到的字符串只是:
Java
不会包含:
\n
也就是说:
readLine()会读取一行内容,但返回结果中不包含行终止符。
这也是为什么我们把文件按行读取后重新写出去时,需要重新执行换行操作。
3.9 BufferedWriter
BufferedWriter 是:
字符缓冲输出流。
构造器:
BufferedWriter(Writer out)
例如:
BufferedWriter bw =
new BufferedWriter(
new FileWriter("result.txt")
);
可以继续使用普通 Writer 已经拥有的:
write()
方法。
例如:
bw.write("Java");
bw.write("MySQL");
bw.write("Redis");
3.10 BufferedWriter 的 newLine()
BufferedWriter 提供了一个非常实用的方法:
newLine()
作用:
写入当前操作系统对应的行分隔符。
例如:
bw.write("Java");
bw.newLine();
bw.write("MySQL");
bw.newLine();
bw.write("Spring Boot");
最终文件:
Java
MySQL
Spring Boot
相比直接写:
bw.write("\n");
newLine() 更能表达:
“我要换行。”
而不是:
“我要写一个特定字符。”
因此在字符缓冲输出流中,应优先认识:
newLine()
3.11 BufferedReader + BufferedWriter
处理文本文件时,两者通常配套出现。
例如复制文本:
import java.io.*;
public class TextCopyDemo {
public static void main(String[] args) throws IOException {
try (
BufferedReader br =
new BufferedReader(
new FileReader("source.txt")
);
BufferedWriter bw =
new BufferedWriter(
new FileWriter("target.txt")
)
) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
}
}
}
数据流向:
source.txt
↓
FileReader
↓
BufferedReader
↓
String
↓
BufferedWriter
↓
FileWriter
↓
target.txt
这里已经开始体现 Java IO 非常重要的一种思想:
通过组合不同的流,让每一层负责一种能力。
四、原理与进阶
4.1 为什么缓冲流通常更高效
假设:
FileInputStream fis = new FileInputStream("a.mp4");
while (fis.read() != -1) {
}
每次只请求:
1 byte
程序需要进行大量读取调用。
而缓冲输入流大致采用:
程序请求数据
↓
缓冲区有数据?
┌────┴────┐
有 没有
↓ ↓
直接返回 从底层流批量读取
↓
放入缓冲区
↓
返回数据
因此很多读取请求可以直接从:
内存缓冲区
获得。
这是缓冲思想的核心。
不过需要注意:
“调用一次
read()就一定访问一次物理磁盘”并不是准确的系统级描述。
操作系统本身也存在页缓存、文件系统缓存等机制。
Java 缓冲流解决的是:
Java IO 调用和数据搬运层面的缓冲问题。
不能简单理解成“每一个 Java read 都对应机械硬盘读一次”。
4.2 装饰器思想
观察:
new BufferedInputStream(
new FileInputStream("a.txt")
);
FileInputStream 提供:
从文件读取字节
BufferedInputStream 提供:
缓冲能力
两者组合:
文件读取能力
+
缓冲能力
以后还可能看到:
new BufferedReader(
new InputStreamReader(
new FileInputStream("a.txt")
)
);
这里出现三层:
FileInputStream
↓
InputStreamReader
↓
BufferedReader
分别负责:
FileInputStream
负责:从文件读取 byte
InputStreamReader
负责:byte → char
BufferedReader
负责:缓冲 + 按行读取
这就是 Java IO 中极其重要的:
流包装 / 装饰器式组合思想。
不要只背类名。
真正应该学会的是:
我现在缺什么能力?
↓
找到负责这种能力的流
↓
把不同流组合起来
4.3 flush() 是什么
输出缓冲流的数据可能暂时保存在缓冲区。
因此存在:
flush()
作用:
把已经缓冲的输出数据向底层输出目标推进。
例如:
bw.write("hello");
bw.flush();
但是正常使用:
close()
关闭输出流时,也会完成必要的刷新。
因此:
正常结束:
close()
程序运行中要求数据立即向下层提交:
flush()
不要形成:
每
write()一次就flush()一次
这样的习惯。
如果频繁刷新,就会削弱缓冲的意义。
五、实践应用
5.1 文件复制
对于图片、视频、压缩包等:
.jpg
.png
.mp4
.zip
.pdf
.class
应当采用:
字节流
常见组合:
BufferedInputStream
+
BufferedOutputStream
+
byte[]
而不是:
Reader / Writer
5.2 文本逐行处理
如果业务是:
读取日志
读取配置
处理 CSV 文本
处理文章
读取代码文件
逐行统计
那么:
BufferedReader.readLine()
通常非常方便。
例如统计文件行数:
import java.io.*;
public class CountLineDemo {
public static void main(String[] args) throws IOException {
try (
BufferedReader br =
new BufferedReader(
new FileReader("article.txt")
)
) {
int count = 0;
while (br.readLine() != null) {
count++;
}
System.out.println("总行数:" + count);
}
}
}
5.3 按行处理并重新写出
例如源文件:
3. Spring Boot
1. Java
2. MySQL
读取:
BufferedReader
存入集合:
List<String>
排序后:
BufferedWriter
重新写出。
核心结构:
文件
↓
BufferedReader
↓
List
↓
排序/过滤/处理
↓
BufferedWriter
↓
新文件
这其实已经把前面学习的:
集合
+
Lambda / Stream
+
File
+
IO
逐渐组合起来了。
六、常见问题
6.1 缓冲流是不是只能操作文件?
不是。
例如:
BufferedInputStream
要求的是:
InputStream
而不是:
FileInputStream
所以以后网络编程中也可能出现:
Socket
↓
InputStream
↓
BufferedInputStream
这就是为什么:
BufferedInputStream(InputStream in)
参数类型设计成父类 InputStream。
6.2 有 BufferedInputStream 后还需要 byte[] 吗?
通常仍然建议批量读取。
例如:
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
不要把:
缓冲流
与:
程序中的 byte[]
理解成二选一。
6.3 BufferedReader 能读取图片吗?
从 API 层面可以读取底层字符来源,但:
不应该使用字符流处理图片等二进制文件。
图片应该使用:
InputStream / OutputStream
因为字符流会涉及:
字节 ↔ 字符
的编码转换。
这可能破坏原始二进制数据。
6.4 readLine() 为什么判断 null?
因为:
null
代表:
已经到达输入末尾,没有下一行。
不是:
读取到空行
空行通常得到:
""
所以:
null ≠ ""
一定要分清。
6.5 newLine() 和 "\n" 一样吗?
目标都是换行,但语义不同。
"\n"
显式写入 LF 字符。
而:
newLine()
写入当前平台对应的行分隔符。
因此字符文件处理中:
newLine()
通常更合适。
6.6 close() 应该关闭哪个流?
如果:
BufferedInputStream bis =
new BufferedInputStream(fis);
通常操作并关闭:
最外层包装流
即可。
关闭包装流会继续关闭其包装的底层流。
例如:
BufferedInputStream.close()
↓
FileInputStream.close()
资源释放的完整机制将在后续 07-13 资源释放与 try-with-resources 中系统整理。
七、练习与验收
7.1 知识问答
- [ ] Java 中四种主要缓冲流分别是什么?
- [ ] 字节缓冲流与字符缓冲流分别处理什么数据?
- [ ] 缓冲流为什么通常可以改善 IO 性能?
- [ ]
BufferedInputStream会不会把字节流变成字符流? - [ ]
BufferedReader最重要的新增方法是什么? - [ ]
readLine()返回的字符串是否包含换行符? - [ ]
readLine()返回null表示什么? - [ ]
BufferedWriter新增的便捷方法是什么? - [ ]
flush()与close()有什么区别? - [ ] 为什么不能认为每一次
FileInputStream.read()都一定直接访问一次物理磁盘? - [ ] 为什么缓冲流体现了 IO 流的包装思想?
- [ ] 为什么复制图片和视频仍然应该使用字节流?
7.2 代码阅读
阅读代码,不运行:
try (
BufferedReader br =
new BufferedReader(
new FileReader("a.txt")
)
) {
String line;
while ((line = br.readLine()) != null) {
System.out.print(line);
}
}
回答:
- [ ] 如果
a.txt中有三行文本,最终控制台一定显示三行吗? - [ ] 为什么?
- [ ] 应该怎样修改?
阅读:
try (
BufferedInputStream bis =
new BufferedInputStream(
new FileInputStream("a.jpg")
)
) {
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
System.out.println(len);
}
}
回答:
- [ ]
len表示什么? - [ ] 最后一次读取时
len一定是 1024 吗? - [ ]
-1表示什么?
7.3 手写代码
关闭 AI 自动补全,独立完成:
- [ ] 使用
BufferedInputStream和BufferedOutputStream复制一张图片。 - [ ] 使用字节数组一次读取多个字节。
- [ ] 使用
BufferedReader按行读取一个.txt文件。 - [ ] 使用
BufferedWriter写出 5 行学生信息。 - [ ] 使用
readLine()统计文本文件总行数。 - [ ] 使用缓冲字符流将一个文本文件逐行复制到另一个文件。
7.4 Debug
下面代码存在问题:
BufferedReader br =
new BufferedReader(
new FileReader("data.txt")
);
String line;
while ((line = br.readLine()) != null) {
System.out.print(line);
}
要求:
- [ ] 找出最终文本格式可能发生的变化。
- [ ] 说明原因。
- [ ] 修复代码。
下面代码:
BufferedOutputStream bos =
new BufferedOutputStream(
new FileOutputStream("a.txt")
);
bos.write("hello".getBytes());
要求:
- [ ] 分析程序结束时为什么不能养成“不关闭资源”的习惯。
- [ ] 给出合理的资源管理结构。
- [ ] 说明
flush()与close()的关系。
7.5 综合训练
设计一个程序:
源文件:students.txt
文件中每行表示一个学生:
1003,王五,85
1001,张三,93
1002,李四,78
要求:
读取文件
→ 按行处理
→ 存入集合
→ 按成绩排序
→ 写入 students_sorted.txt
限制:
- 输入必须使用
BufferedReader - 输出必须使用
BufferedWriter - 必须保留一条学生记录占一行
- 不允许一次把整个文件读成一个字符串
先完成:
数据流设计
→ 类/数据模型设计
→ 伪代码
→ Java 实现
7.6 本章验收
不查看资料,能够独立写出:
BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
四种流的创建方式。
能够解释:
缓冲流解决什么问题
↓
为什么提高性能
↓
readLine() 有什么价值
↓
newLine() 有什么价值
↓
字节缓冲流和字符缓冲流怎么选择
最后能够独立完成:
使用字节缓冲流复制任意二进制文件,并使用字符缓冲流逐行处理文本文件。
达到以上标准,本章核心知识即掌握。