1. 引言
回顾一下 FileReader 和 FileWriter 是如何工作的。每次调用它们的 read() 或 write() 方法,都会访问文件系统——也就是说,计算机会真实地去磁盘上读取或写入一个字符。如果文件很大,而你一次只读/写一个字符,这就会变成成千上万、甚至上百万次磁盘访问的“马拉松”。而众所周知,磁盘并不是程序员最快的朋友。
可以这样想象:你要把一桶水倒进瓶子里,却用的是茶匙。形式上可行,但非常慢。更明智的做法是用勺或杯子。处理文件亦然:逐字符读写,就像用茶匙搬水。
如下所示:
// 按字符读取文件(就像用 "茶匙"!)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// 处理字符(例如,仅统计数量)
}
}
如果文件很大,你会注意到程序运行非常缓慢。
缓冲:是什么以及为什么需要
缓冲区是内存中的一块特殊区域(通常是数组),数据会被成块地读入或写出(例如每次 8 KB 或更多),而不是逐字符处理。简单说,缓冲区就是一块中间存储的内存,相当于“中间水桶”,以更大的批次读写数据。
工作流程是这样的:读取时,程序一次访问磁盘,把整块数据装入缓冲区,然后再从内存中按需把字符一个个“发”给你。一旦这一块用完,就再加载下一块。写入时也一样:数据先放入缓冲区,然后再成块写入磁盘(或者在你调用 flush() 时立即写出)。
为什么更快?因为磁盘本身就慢,尤其是被频繁“小打小闹”时。而减少访问次数、每次多拿一些,会快得多。简而言之:缓冲能减少对磁盘的访问次数,从而加速程序。
2. BufferedReader 和 BufferedWriter:语法与示例
在 Java 中,用于对文本文件读写进行缓冲的两个类是:
- BufferedReader — 用于读取文本文件。
- BufferedWriter — 用于写入文本文件。
它们构建在普通的 Reader/Writer(如 FileReader/FileWriter)之上,增加了缓冲能力。
使用 BufferedReader 按行读取文件
最常见的场景是按行读取。方法 readLine() 会返回直到换行符为止的一整行("\n" 或 "\r\n")。
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 将该行输出到屏幕
}
} catch (IOException e) {
System.out.println("读取文件时出错:" + e.getMessage());
}
}
}
这里发生了什么?
我们创建了一个 FileReader,它能逐字符读取文件,然后把它包在 BufferedReader 中。缓冲读取器不是逐字符地拿数据,而是成块拿到内存,再通过 readLine() 按行“分发”给你。最终你只需要写一个 while 循环,逐行获取字符串,不必关心文件有多大:读取依然会高效而节省开销。
使用 BufferedWriter 写入文件
把字符串写入文件同样可以很高效:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, world!");
writer.newLine(); // 换行(取决于 OS)
writer.write("这是第二行。");
} catch (IOException e) {
System.out.println("写入文件时出错:" + e.getMessage());
}
}
}
这里发生了什么?
我们先创建 FileWriter,再把它包在 BufferedWriter 中。当你调用 write() 或 newLine() 时,数据不会立刻写入磁盘,而是先放到缓冲区(内存中的一层“垫片”)。只有当缓冲区被写满、你关闭流(或显式调用 flush())时,累积的文本才会成批写入文件。这种方式显著加快写入速度并减少磁盘访问。
在一个完整应用中是什么样?
假设我们编写一个简单的“日记”程序,把输入保存到文件并在屏幕上显示出来。
import java.io.*;
import java.util.Scanner;
public class DiaryApp {
public static void main(String[] args) {
String fileName = "diary.txt";
Scanner scanner = new Scanner(System.in);
// 写入新记录
System.out.print("请输入一条新的日记记录:");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("记录已保存!");
} catch (IOException e) {
System.out.println("写入时出错:" + e.getMessage());
}
// 读取所有记录
System.out.println("\n您的日记:");
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("读取时出错:" + e.getMessage());
}
}
}
我们使用文件名 "diary.txt",提示用户输入一条新记录。为保存数据,我们以 append 模式使用 FileWriter(传入 true),因此旧的内容不会被覆盖——每条新记录都会整齐地追加到文件末尾。再包上一层 BufferedWriter,写入就会更快更省 I/O:数据先在内存中累积,然后成块写入磁盘。
随后我们通过 BufferedReader 打开同一个文件进行读取。它会成块获取内容,再一行一行地交给你。循环中程序只是把每一行打印到屏幕上,最终你就能从头到尾看到自己的“日记”。
3. BufferedReader 和 BufferedWriter 的优势
显著加速
使用带缓冲的流进行读写时,程序对磁盘的访问次数会减少几十倍,甚至上百倍。这在大文件上尤其明显。
便捷的方法
- BufferedReader.readLine() — 支持按行读取,处理文本文件(如日志、CSV、配置)非常方便。
- BufferedWriter.newLine() — 添加换行,并会根据操作系统选择合适的换行符。
易于使用
- 可以与其他流轻松组合(例如,将 InputStreamReader 包在 BufferedReader 中,以读取不同编码的文件)。
- 配合 try-with-resources 使用可自动关闭所有资源。
灵活
- 如果默认的缓冲区大小(通常为 8 KB)不合适,你可以显式指定:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // 16 KB 缓冲区
4. 何时使用 BufferedReader 和 BufferedWriter
建议使用:
- 当处理文本文件(日志、CSV、大量文本数据)。
- 当需要按行读取或写入文件。
- 当处理大文件且性能重要时,以加速 I/O。
- 当需要处理来自网络或其他支持 Reader/Writer 的数据源时。
不建议使用:
- 处理二进制文件(如图片、压缩包、视频)——应使用 InputStream/OutputStream。
- 如果文件非常小且只读/写一次完整内容——缓冲带来的收益很小(但通常也无害)。
5. 实用细节
与不同字符编码配合
如果需要以特定编码读/写文件(例如 "UTF-8"、"Windows-1251"),请将 InputStreamReader/OutputStreamWriter 与带缓冲的流组合使用:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
显式刷新缓冲区
有时你需要确保数据已落盘(例如写日志或收据)。此时调用 writer.flush()。通常不必手动调用,因为关闭流会自动刷新缓冲区。
缓冲区大小
默认缓冲区大约是 8 KB。若你确信调整大小能带来性能收益(例如处理超大文件时),可以自行设置。
对比:FileReader/FileWriter 与 BufferedReader/BufferedWriter
| 类 | 大文件上的速度 | 按行读取的便利性 | 按行写入的便利性 | 编码灵活性 |
|---|---|---|---|---|
| FileReader/FileWriter | 慢 | 否(只能逐字符) | 否(只能逐字符) | 仅默认 |
| BufferedReader/Writer | 快 | 是(readLine()) | 是(newLine()) | 是(通过 InputStreamReader/OutputStreamWriter) |
6. 使用 BufferedReader 和 BufferedWriter 时的常见错误
错误 1:忘记关闭流。 如果不使用 try-with-resources 或不调用 close(),文件可能保持锁定,数据也可能未写入磁盘。请务必使用 try-with-resources!
错误 2:把文本与二进制的处理方式混淆。 试图通过 BufferedReader 打开二进制文件(".jpg"、".zip")会得到乱码并且很可能报错。处理二进制文件应使用 InputStream/OutputStream。
错误 3:在大数据量场景下不使用缓冲。 逐字符读/写会让程序很慢。处理大文件时务必使用缓冲。
错误 4:需要时却没有调用 flush()。 如果需要数据立即落盘(例如用于日志),请调用 writer.flush()。不过通常关闭流就会自动刷新。
错误 5:忽略编码问题。 如果用错误的编码打开文件,文本可能显示不正确(俄文字母会变成“?”、奇怪字符等)。当编码与系统默认不同,请始终显式指定需要的编码(例如 "UTF-8")。
GO TO FULL VERSION