缓冲流
小崔测试完两种文件复制方案后感叹:"一个8KB的字节数组就让速度提升了40多倍!"
李眉:"这就是缓冲的力量。Java已经把这种缓冲逻辑封装成了现成的类——BufferedInputStream和BufferedReader,你不用自己管理缓冲区数组,直接享受性能红利。"
一、缓冲流定位
缓冲流是典型的处理流(装饰器),它在内部维护一个缓冲区(默认8KB),将多次小的读/写操作合并为少量大的物理操作,从而大幅减少系统调用次数。
四种缓冲流速查
| 缓冲流 | 对应基类 | 缓冲区默认大小 | 特有方法 |
|---|---|---|---|
| BufferedInputStream | InputStream | 8192 bytes (8KB) | 无特别额外方法 |
| BufferedOutputStream | OutputStream | 8192 bytes (8KB) | flush() |
| BufferedReader | Reader | 8192 chars (16KB) | readLine() 按行读取 |
| BufferedWriter | Writer | 8192 chars (16KB) | newLine() 写出换行符 |
二、核心类定义与构造方法
2.1 BufferedInputStream / BufferedOutputStream
| 构造方法 | 说明 |
|---|---|
BufferedInputStream(InputStream in) | 默认8KB缓冲区 |
BufferedInputStream(InputStream in, int size) | 指定缓冲区大小(字节) |
BufferedOutputStream(OutputStream out) | 默认8KB缓冲区 |
BufferedOutputStream(OutputStream out, int size) | 指定缓冲区大小(字节) |
2.2 BufferedReader / BufferedWriter
| 构造方法 | 说明 |
|---|---|
BufferedReader(Reader in) | 默认8KB字符缓冲区 |
BufferedReader(Reader in, int sz) | 指定缓冲区大小(字符) |
BufferedWriter(Writer out) | 默认8KB字符缓冲区 |
BufferedWriter(Writer out, int sz) | 指定缓冲区大小(字符) |
2.3 特有方法
| 方法 | 所属类 | 返回值 | 说明 |
|---|---|---|---|
String readLine() | BufferedReader | 一行字符串(不含换行符),流结束返回null | 按行读取,极大简化文本处理 |
void newLine() | BufferedWriter | void | 写出平台相关的换行符(Windows=\r\n, Linux=\n) |
void flush() | BufferedOutputStream / BufferedWriter | void | 强制将缓冲区数据写出到被装饰的流 |
三、缓冲原理深度分析
3.1 BufferedReader 的 readLine() 实现原理
关键:
readLine()返回的字符串不包含换行符。写入时如果需要换行,使用newLine()而不是手动写\n。
3.2 BufferedOutputStream 的缓冲写入机制
// BufferedOutputStream 核心逻辑(简化版)
public class BufferedOutputStream extends FilterOutputStream {
protected byte[] buf; // 内部缓冲区
protected int count; // 当前已缓冲的字节数
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer(); // 缓冲区满,批量写出到被装饰的流
}
buf[count++] = (byte) b; // 只写到内部数组,不触发物理写入
}
public synchronized void flush() throws IOException {
flushBuffer(); // 强制写出
out.flush(); // 再flush底层流
}
private void flushBuffer() throws IOException {
if (count > 0) {
out.write(buf, 0, count); // 一次性批量写出
count = 0;
}
}
}
write(int b)不是每次触发系统调用——它只把字节放进内部数组。只有当数组满了,或者调用flush()/close(),才真正写出。
3.3 缓冲区大小对性能的影响
| 缓冲区大小 | 1MB文件系统调用次数(读) | 典型耗时 | 内存开销 |
|---|---|---|---|
| 无缓冲(逐字节) | 1,048,576 | ~180 ms | 0 |
| 4KB | 256 | ~5 ms | 4KB |
| 8KB(默认) | 128 | ~3 ms | 8KB |
| 64KB | 16 | ~2 ms | 64KB |
| 1MB | 1 | ~2 ms | 1MB |
经验法则:默认8KB对大多数场景足够。特殊场景(如SSD上的大文件读写)可适当增大到64KB。过大(如10MB)浪费内存且边际收益接近于零。
四、完整示例
示例一:飞翔科技日志文件按行读取分析
场景:飞翔科技运维团队需要分析系统日志 system.log,统计 ERROR 级别的日志数量并提取所有错误行。
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* 飞翔科技 - 日志分析工具
* 使用 BufferedReader.readLine() 按行读取日志
*/
public class LogAnalyzer {
public static void main(String[] args) {
String logPath = "C:/feixiang/logs/system.log";
List<String> errorLines = new ArrayList<>();
int totalLines = 0;
int errorCount = 0;
int warnCount = 0;
int infoCount = 0;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(logPath), "UTF-8"))) {
String line;
while ((line = br.readLine()) != null) {
totalLines++;
if (line.contains("[ERROR]")) {
errorCount++;
errorLines.add(line);
} else if (line.contains("[WARN]")) {
warnCount++;
} else if (line.contains("[INFO]")) {
infoCount++;
}
}
} catch (IOException e) {
System.err.println("日志分析失败:" + e.getMessage());
return;
}
// 输出统计报告
System.out.println("=== 飞翔科技系统日志分析报告 ===");
System.out.println("日志文件路径:" + logPath);
System.out.println("总行数:" + totalLines);
System.out.println("--------------------------------");
System.out.printf("INFO : %d 条%n", infoCount);
System.out.printf("WARN : %d 条%n", warnCount);
System.out.printf("ERROR : %d 条%n", errorCount);
System.out.println("--------------------------------");
if (!errorLines.isEmpty()) {
System.out.println("\n错误日志详情:");
for (int i = 0; i < errorLines.size(); i++) {
System.out.printf(" #%d: %s%n", (i + 1), errorLines.get(i));
}
}
}
}
运行输出:
=== 飞翔科技系统日志分析报告 ===
日志文件路径:C:/feixiang/logs/system.log
总行数:1523
--------------------------------
INFO : 1420 条
WARN : 87 条
ERROR : 16 条
--------------------------------
错误日志详情:
#1: [2025-01-15 09:30:13] [ERROR] 数据库连接超时 - 主机:192.168.1.200:3306
#2: [2025-01-15 10:22:45] [ERROR] 用户认证失败 - 用户ID:10582
...
示例二:飞翔科技员工CSV文件读写(带缓冲)
场景:小崔需要读取员工CSV文件,筛选出特定部门的员工,写入新文件。
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 飞翔科技 - 部门员工筛选工具
* 使用 BufferedReader/BufferedWriter 高效处理CSV文件
*/
public class EmployeeFilter {
static class Employee {
String id;
String name;
String department;
int age;
Employee(String id, String name, String department, int age) {
this.id = id;
this.name = name;
this.department = department;
this.age = age;
}
String toCsv() {
return String.join(",", id, name, department, String.valueOf(age));
}
}
public static void main(String[] args) {
String inputPath = "C:/feixiang/data/employees.csv";
String outputPath = "C:/feixiang/data/tech_dept_employees.csv";
List<Employee> techEmployees = new ArrayList<>();
// 第一步:使用 BufferedReader 读取并筛选
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(inputPath), StandardCharsets.UTF_8))) {
// 读取并跳过表头
String header = br.readLine();
if (header == null) {
System.err.println("CSV文件为空!");
return;
}
String line;
while ((line = br.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length >= 4) {
String dept = parts[2].trim();
// 筛选技术部员工
if ("技术部".equals(dept) || "技术总监".equals(dept)) {
techEmployees.add(new Employee(
parts[0].trim(),
parts[1].trim(),
dept,
Integer.parseInt(parts[3].trim())
));
}
}
}
} catch (IOException e) {
System.err.println("CSV读取失败:" + e.getMessage());
return;
}
// 第二步:使用 BufferedWriter 写出筛选结果
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(outputPath), StandardCharsets.UTF_8))) {
// 写表头
bw.write("员工ID,姓名,部门,年龄");
bw.newLine();
// 写数据行
for (Employee emp : techEmployees) {
bw.write(emp.toCsv());
bw.newLine();
}
System.out.println("=== 筛选结果 ===");
System.out.println("技术部员工共 " + techEmployees.size() + " 人");
System.out.println("已输出到:" + outputPath);
System.out.println("\n技术部员工列表:");
for (Employee emp : techEmployees) {
System.out.printf(" %s | %s | %s | %d岁%n",
emp.id, emp.name, emp.department, emp.age);
}
} catch (IOException e) {
System.err.println("CSV写入失败:" + e.getMessage());
}
}
}
假设 employees.csv:
员工ID,姓名,部门,年龄
E001,大翔,技术经理,38
E002,白歌,技术部,32
E003,小崔,技术部,24
E004,李眉,技术部,29
E005,小张,市场部,27
E006,小王,财务部,31
运行输出:
=== 筛选结果 ===
技术部员工共 4 人
已输出到:C:/feixiang/data/tech_dept_employees.csv
技术部员工列表:
E001 | 大翔 | 技术经理 | 38岁
E002 | 白歌 | 技术部 | 32岁
E003 | 小崔 | 技术部 | 24岁
E004 | 李眉 | 技术部 | 29岁
五、flush() 方法深度解析
| 情况 | 数据是否落盘 |
|---|---|
| 写入8KB,缓冲区8KB满 | 是(自动触发) |
| 写入1KB,未flush,未close | 否(数据在内存中!) |
| 写入1KB,调用flush() | 是 |
| 写入1KB,调用close() | 是(close内部调用flush) |
重要:
BufferedWriter.flush()只负责把 Writer 缓冲区刷到底层流。如果底层是BufferedOutputStream,还需要再flush一次。使用 try-with-resources 时,close()按逆序调用,各层都会自动 flush。
六、易错场景
反例一:忘记关闭缓冲输出流导致文件内容不完整
// ❌ 错误:程序退出前未关闭流,缓冲区数据丢失
BufferedWriter bw = new BufferedWriter(new FileWriter("report.txt"));
bw.write("飞翔科技月度报告");
bw.write("本期营收:500万元");
// 没有 bw.close() —— 文件可能为空或内容不完整!
System.exit(0);
纠正:
// ✅ 正确:try-with-resources 确保 close()
try (BufferedWriter bw = new BufferedWriter(new FileWriter("report.txt"))) {
bw.write("飞翔科技月度报告");
bw.newLine();
bw.write("本期营收:500万元");
} // 自动 close → flush → 数据完整落盘
反例二:readLine() 返回的字符串不包含换行符,写出时忘记补
// ❌ 错误:读出后直接写回,换行符丢失,所有行挤在一行
try (BufferedReader br = ...; BufferedWriter bw = ...) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line); // 忘记 bw.newLine();
}
}
纠正:
// ✅ 正确:使用 newLine() 补充换行符
try (BufferedReader br = ...; BufferedWriter bw = ...) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 补充平台相关的换行符
}
}
反例三:BufferedReader 和 BufferedInputStream 用混
// ❌ 错误:BufferedReader 需要 Reader 参数,不接收 InputStream
BufferedReader br = new BufferedReader(new FileInputStream("data.txt"));
// 编译错误!BufferedReader 的构造方法是 BufferedReader(Reader in)
纠正:
// ✅ 正确:通过 InputStreamReader 搭桥
BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("data.txt"), "UTF-8"));
七、面试考点
Q1:BufferedReader 的 readLine() 如何判断文件结束?
返回
null表示文件已读完。注意与read()返回-1区分——read()是单个字符读取,用-1;readLine()是按行读取,用null。典型循环:String line; while ((line = br.readLine()) != null) { // 处理 line }
Q2:BufferedWriter 写入后一定要 flush() 吗?
不一定每次都需要,但取决于数据可靠性要求。如果使用 try-with-resources,
close()会自动flush(),无需手动调用。但如果是持续写入的日志文件(流不关闭),则需要定期flush()确保数据落盘。也可以构造时指定较小的缓冲区,使其更频繁地自动刷新。
Q3:BufferedInputStream 为什么没有 readLine() 方法?
readLine()是按字符行读取,需要识别换行符(\n、\r\n),这涉及字符解码。BufferedInputStream操作的是字节,不知道字节序列在编码后对应的字符边界在哪里,无法可靠地识别"行"。BufferedReader操作的是解码后的字符,可以安全地扫描换行符。
Q4:缓冲区大小设置为多少最合适?
默认 8KB 适合绝大多数场景。选择依据:① 磁盘块大小:通常 4KB 或 8KB,缓冲区为此值的整数倍效率最高;② 读写模式:顺序大文件操作用较大缓冲区(64KB~256KB),随机小文件操作用默认即可;③ 内存敏感场景:嵌入式或移动设备用较小缓冲区(1KB~4KB)。
Q5:close() 和 flush() 的区别是什么?
flush()只将缓冲区数据强制写出到目的地,流仍然可用;close()会先调用flush(),然后释放底层操作系统资源(文件句柄、网络连接等),此后流不可再用。对于需要长时间打开的文件(如日志),应定期flush()而不是频繁close()再重新打开。
李眉:"缓冲流是性能优化的利器,几乎任何IO操作都应该加上缓冲。但有一个问题没解决——字节流和字符流之间怎么互转?下一节我们看转换流这个'桥梁'。"