Skip to content

BufferedReader 高效读取文本

你有没有想过:为什么 BufferedReader.readLine() 能一次性读一整行,而不是逐字符返回?

答案在缓冲区。

为什么需要缓冲

先看一个反直觉的事实:

无 BufferedReader:
  读取 1000 个字符 = 1000 次系统调用(每次读 1 字符)

BufferedReader(默认 8KB 缓冲区):
  读取 1000 个字符 = 1 次系统调用(一次读 8192 字符到内存)
  后续 999 个字符从内存缓冲区取,不触发系统调用

系统调用是有开销的。一次 read() 从用户态进入内核态,涉及到上下文切换。每次只读一个字符,就要切换一千次;批量读八千字符,只需要切换一次。

缓冲的本质:用一次大的系统调用,换多次小的系统调用。

readLine() 为什么高效

BufferedReaderreadLine() 并不是一行一行从磁盘读的。它的内部逻辑是:

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()(整行读取)。 读取文本文件,永远用它。

基于 VitePress 构建