【Java源码阅读系列37】深度解读Java BufferedReader 源码

Java 的 BufferedReader 是字符输入流的缓冲装饰类,通过内置缓冲区减少底层 IO 操作次数,显著提升文本读取效率。本文基于 JDK 1.8 源码,从类结构、核心机制、关键方法到设计模式,全面解析其实现逻辑。

一、类结构与核心成员

BufferedReader 继承自抽象类 Reader,实现了字符输入流的缓冲功能。其核心成员变量如下:

java 复制代码
public class BufferedReader extends Reader {
    private Reader in;          // 被装饰的底层字符输入流
    private char cb[];          // 字符缓冲区(默认大小 8192)
    private int nChars;         // 缓冲区中有效字符数
    private int nextChar;       // 下一个待读取字符的位置
    private int markedChar;     // 标记位置(用于 mark/reset)
    private int readAheadLimit; // 标记后允许读取的最大字符数
    private boolean skipLF;     // 是否跳过下一个换行符(处理 \r\n 场景)
    // ... 其他静态常量与方法 ...
}
  • in:被包装的底层字符流(如 FileReader),所有 IO 操作最终委托给它。
  • cb:缓冲区数组,存储从底层流预读取的字符,减少 read() 调用次数。
  • markedCharreadAheadLimit:支持标记(mark())和重置(reset())功能,允许回退到标记位置重新读取。

二、构造函数:灵活的缓冲初始化

BufferedReader 提供两个构造函数,支持自定义缓冲区大小或使用默认值(8192 字符):

java 复制代码
public BufferedReader(Reader in, int sz) {
    super(in);  // 调用父类 Reader 的构造函数(初始化锁对象)
    if (sz <= 0) throw new IllegalArgumentException("Buffer size <= 0");
    this.in = in;
    cb = new char[sz];  // 初始化指定大小的缓冲区
    nextChar = nChars = 0;  // 初始无有效字符
}

public BufferedReader(Reader in) {
    this(in, defaultCharBufferSize);  // 默认缓冲区大小 8192
}

设计要点

通过装饰器模式包装底层 Reader,将用户与底层流的直接交互隔离,用户只需操作 BufferedReader 即可获得缓冲能力。


三、核心机制:缓冲区填充与读取优化

BufferedReader 的核心逻辑围绕"预读取-缓冲-按需消费"展开,关键方法是 fill()(填充缓冲区)和 read()(从缓冲区读取)。

1. fill():缓冲区填充策略

当缓冲区数据不足(nextChar >= nChars)时,fill() 负责从底层流读取数据填充缓冲区。若存在有效标记(markedChar),需考虑标记位置的保留:

java 复制代码
private void fill() throws IOException {
    if (markedChar <= UNMARKED) {  // 无标记:直接覆盖缓冲区
        dst = 0;
    } else {
        int delta = nextChar - markedChar;
        if (delta >= readAheadLimit) {  // 超过标记允许的读取范围:失效标记
            markedChar = INVALIDATED;
            dst = 0;
        } else {
            // 标记有效:调整缓冲区,保留标记位置到当前位置的数据
            System.arraycopy(cb, markedChar, cb, 0, delta);
            markedChar = 0;
            dst = delta;
        }
    }
    // 从底层流读取数据到缓冲区(dst 为填充起始位置)
    int n = in.read(cb, dst, cb.length - dst);
    if (n > 0) {
        nChars = dst + n;  // 更新有效字符数
        nextChar = dst;    // 重置下一个读取位置
    }
}

关键逻辑

  • 无标记时,缓冲区从头开始填充;
  • 有标记时,若未超出 readAheadLimit,则将标记位置后的数据前移,保留上下文;
  • 最终调用底层流的 read() 填充缓冲区剩余空间。

2. read():单字符读取

read() 方法从缓冲区读取单个字符,若缓冲区空则调用 fill() 填充:

java 复制代码
public int read() throws IOException {
    synchronized (lock) {  // 同步保证线程安全(继承自 Reader 的 lock 对象)
        ensureOpen();       // 检查流是否关闭
        for (;;) {
            if (nextChar >= nChars) {  // 缓冲区空,填充
                fill();
                if (nextChar >= nChars) return -1;  // 填充后仍空:EOF
            }
            if (skipLF) {  // 跳过前一个 \r 后的 \n
                skipLF = false;
                if (cb[nextChar] == '\n') {
                    nextChar++;
                    continue;
                }
            }
            return cb[nextChar++];  // 返回当前字符并移动指针
        }
    }
}

优化点

通过 skipLF 状态处理 \r\n 换行符(如 Windows 系统),避免重复读取换行符。

3. readLine():行读取的核心实现

readLine() 是最常用的方法之一,用于读取一行文本(以 \n\r\r\n 结尾):

java 复制代码
public String readLine() throws IOException {
    return readLine(false);  // 调用重载方法,不跳过初始换行符
}

String readLine(boolean ignoreLF) throws IOException {
    StringBuffer s = null;
    int startChar;
    synchronized (lock) {
        ensureOpen();
        boolean omitLF = ignoreLF || skipLF;  // 是否跳过初始换行符

        bufferLoop:
        for (;;) {
            if (nextChar >= nChars) fill();  // 缓冲区空则填充
            if (nextChar >= nChars) return (s != null && s.length() > 0) ? s.toString() : null;  // EOF

            boolean eol = false;  // 是否遇到行终止符
            int i;
            // 查找行终止符(\n 或 \r)
            for (i = nextChar; i < nChars; i++) {
                if (cb[i] == '\n' || cb[i] == '\r') {
                    eol = true;
                    break;
                }
            }

            startChar = nextChar;
            nextChar = i;  // 移动到行终止符位置

            if (eol) {  // 处理行终止符
                String str = (s == null) 
                    ? new String(cb, startChar, i - startChar)  // 直接截取缓冲区内容
                    : s.append(cb, startChar, i - startChar).toString();  // 拼接已有内容
                nextChar++;  // 跳过当前终止符
                if (cb[i] == '\r') skipLF = true;  // 标记下一个 \n 需跳过(处理 \r\n)
                return str;
            }

            // 未找到终止符:将缓冲区内容追加到 StringBuffer
            if (s == null) s = new StringBuffer(defaultExpectedLineLength);  // 默认行长度 80
            s.append(cb, startChar, i - startChar);
        }
    }
}

关键逻辑

  • 循环读取缓冲区,直到找到行终止符或缓冲区耗尽;
  • 使用 StringBuffer 拼接跨缓冲区的行内容(如一行数据分多次填充到缓冲区);
  • 处理 \r\n 组合换行符(通过 skipLF 标记跳过后续的 \n)。

四、标记与重置:mark()reset()

BufferedReader 支持标记当前位置(mark()),并通过 reset() 回退到标记位置重新读取,这依赖缓冲区对标记位置的保留:

java 复制代码
public void mark(int readAheadLimit) throws IOException {
    if (readAheadLimit < 0) throw new IllegalArgumentException("Read-ahead limit < 0");
    synchronized (lock) {
        ensureOpen();
        this.readAheadLimit = readAheadLimit;  // 标记后允许读取的最大字符数
        markedChar = nextChar;                 // 记录当前读取位置
        markedSkipLF = skipLF;                 // 记录当前换行符跳过状态
    }
}

public void reset() throws IOException {
    synchronized (lock) {
        ensureOpen();
        if (markedChar < 0) throw new IOException(markedChar == INVALIDATED ? "Mark invalid" : "Stream not marked");
        nextChar = markedChar;  // 回退到标记位置
        skipLF = markedSkipLF;  // 恢复换行符跳过状态
    }
}

设计限制

  • readAheadLimit 需合理设置:若超过缓冲区大小,会触发缓冲区扩容(fill() 中重新分配数组);
  • 若标记后读取的字符数超过 readAheadLimit,标记失效(markedChar 设为 INVALIDATED)。

五、设计模式分析

BufferedReader 的源码中体现了多种经典设计模式,是理解 Java IO 框架的关键。

1. 装饰器模式(Decorator Pattern)

BufferedReader 是装饰器模式的典型应用:

  • 核心角色Reader 是抽象组件(Component),BufferedReader 是具体装饰器(Concrete Decorator);
  • 功能增强 :通过包装底层 Reader(如 FileReader),为其添加缓冲功能,用户无需修改原 Reader 代码即可获得性能提升。
java 复制代码
// 装饰器模式示例:将 FileReader 包装为 BufferedReader
Reader fileReader = new FileReader("test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);  // 增强缓冲能力

2. 模板方法模式(Template Method)

Reader 作为抽象基类,定义了 read()close() 等模板方法,子类(如 BufferedReaderFileReader)实现具体逻辑:

  • 统一接口 :用户只需调用 read(),无需关心底层是缓冲流还是原始流;
  • 扩展灵活 :新增字符流类型(如 StringReader)时,只需继承 Reader 并实现抽象方法。

3. 迭代器模式(Iterator Pattern)(Java 8+)

Java 8 的 lines() 方法返回 Stream<String>,内部通过迭代器懒加载行数据:

java 复制代码
public Stream<String> lines() {
    Iterator<String> iter = new Iterator<String>() {
        String nextLine = null;
        public boolean hasNext() {  // 懒加载:仅在需要时读取下一行
            try { return (nextLine != null) || ((nextLine = readLine()) != null); }
            catch (IOException e) { throw new UncheckedIOException(e); }
        }
        public String next() { /* 实现略 */ }
    };
    return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iter, ...), false);
}

优势 :通过迭代器将行读取转换为流式处理,支持 filter()map() 等函数式操作,符合 Java 8 函数式编程趋势。


六、总结

BufferedReader 是 Java IO 体系中"缓冲优化"的经典实现,其核心价值在于通过预读取和缓冲区管理,将多次底层 IO 操作合并为单次,显著提升文本读取效率。结合源码分析,其设计特点可总结为:

  • 缓冲机制 :通过 cb 缓冲区减少底层 read() 调用次数,降低 IO 开销;
  • 状态管理skipLFmarkedChar 等变量精细控制换行符处理和标记重置逻辑;
  • 模式应用:装饰器模式实现功能扩展,模板方法统一接口,迭代器模式支持流式处理;
  • 线程安全 :通过 synchronized (lock) 保证多线程下的操作原子性(lock 继承自 Reader)。

理解 BufferedReader 的源码,不仅能掌握高效文本读取的底层逻辑,更能深入体会设计模式在框架中的实际应用,为高性能 IO 编程和问题排查提供坚实基础。

相关推荐
饼干,20 分钟前
第23天python内容
开发语言·python
这周也會开心32 分钟前
SpringMVC整理
java·springmvc
東雪木33 分钟前
Spring Boot 2.x 集成 Knife4j (OpenAPI 3) 完整操作指南
java·spring boot·后端·swagger·knife4j·java异常处理
数学难36 分钟前
Java面试题2:Java线程池原理
java·开发语言
Charles_go37 分钟前
C#8、有哪些访问修饰符
java·前端·c#
qwer12321ck7640 分钟前
srcType instanceof Class 及泛型 vs 普通类
java
咸鱼求放生41 分钟前
Java 8 Stream API
java·开发语言
盒马盒马43 分钟前
Rust:Trait 抽象接口 & 特征约束
开发语言·rust
天使街23号44 分钟前
go-dongle v1.2.0 发布,新增 SM2 非对称椭圆曲线加密算法支持
开发语言·后端·golang
ThreeYear_s1 小时前
【FPGA+DSP系列】——MATLAB simulink仿真三相桥式全控整流电路
开发语言·matlab·fpga开发