BufferedReader 高效读取文本
你有没有想过:为什么 BufferedReader.readLine() 能一次性读一整行,而不是逐字符返回?
答案在缓冲区。
为什么需要缓冲
先看一个反直觉的事实:
无 BufferedReader:
读取 1000 个字符 = 1000 次系统调用(每次读 1 字符)
BufferedReader(默认 8KB 缓冲区):
读取 1000 个字符 = 1 次系统调用(一次读 8192 字符到内存)
后续 999 个字符从内存缓冲区取,不触发系统调用系统调用是有开销的。一次 read() 从用户态进入内核态,涉及到上下文切换。每次只读一个字符,就要切换一千次;批量读八千字符,只需要切换一次。
缓冲的本质:用一次大的系统调用,换多次小的系统调用。
readLine() 为什么高效
BufferedReader 的 readLine() 并不是一行一行从磁盘读的。它的内部逻辑是:
1. 从缓冲区批量读 8KB 数据
2. 在内存中找换行符位置
3. 返回从当前位置到换行符之间的字符串
4. 如果缓冲区没有换行符,再批量读 8KB,继续找这样,即使文件很大,也只需要少数几次系统调用。
基本用法
java
// 最常见的用法
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}readLine() 的特点:
- 返回
null表示流结束 - 不包含行尾的换行符
\n或回车换行符\r\n - 空行返回空字符串
""(不是null)
按行处理的多种方式
while 循环(最常用)
java
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
processLine(line);
}
}Stream API(JDK 8+)
java
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
reader.lines()
.filter(line -> !line.trim().isEmpty())
.map(String::trim)
.forEach(System.out::println);
}收集到 List
java
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
List<String> lines = reader.lines()
.collect(Collectors.toList());
}带行号读取
java
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream("data.txt"), StandardCharsets.UTF_8))) {
int lineNumber = 0;
String line;
while ((line = reader.readLine()) != null) {
lineNumber++;
System.out.printf("%4d: %s%n", lineNumber, line);
}
}lines() 的陷阱
BufferedReader.lines() 返回的是 Java 9 引入的 Stream<String>,这是一个惰性流——在流关闭时才真正读取数据。
java
// ❌ 问题:流关闭了才读数据
BufferedReader reader = new BufferedReader(
new FileReader("data.txt"));
Stream<String> stream = reader.lines(); // 惰性流,此时还没有读任何数据
reader.close(); // 关闭流
stream.forEach(System.out::println); // 流已关闭,读不到数据!
// ✅ 正确:在 try-with-resources 中使用
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt"))) {
reader.lines()
.forEach(System.out::println);
}
// try 退出时关闭 reader,流也被关闭,数据已读完mark() 和 reset():流的「时光机」
BufferedReader 支持 mark/reset,可以在流中「回退」:
java
try (BufferedReader reader = new BufferedReader(
new FileReader("data.txt"))) {
reader.mark(1024); // 标记,后续最多回退 1024 字符
String line1 = reader.readLine();
String line2 = reader.readLine();
reader.reset(); // 回退到标记位置
String reReadLine1 = reader.readLine(); // 重新读第一行
}这个功能在需要「预读判断」的场景很有用,比如判断文件类型前先读几个字节。
常用方法速查
| 方法 | 说明 |
|---|---|
read() | 读 1 字符 |
read(char[] cbuf) | 批量读到字符数组 |
readLine() | 读一行,返回 String(不含换行符) |
skip(long n) | 跳过 n 字符 |
mark(int readAheadLimit) | 标记位置(最多回退这么多字符) |
reset() | 重置到标记位置 |
lines() | 返回 Stream<String>(JDK 9+) |
ready() | 是否准备好读取(不保证有数据) |
close() | 关闭流(自动关闭底层流) |
记住这个模式:
BufferedReader = 缓冲(减少系统调用)+ readLine()(整行读取)。 读取文本文件,永远用它。
