【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 编程和问题排查提供坚实基础。

相关推荐
Eiceblue24 分钟前
【免费.NET方案】CSV到PDF与DataTable的快速转换
开发语言·pdf·c#·.net
好奇的菜鸟1 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
m0_555762901 小时前
Matlab 频谱分析 (Spectral Analysis)
开发语言·matlab
DuelCode2 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
浪裡遊2 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
优创学社22 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
幽络源小助理2 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码2 小时前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot
lzb_kkk3 小时前
【C++】C++四种类型转换操作符详解
开发语言·c++·windows·1024程序员节
YuTaoShao3 小时前
【LeetCode 热题 100】48. 旋转图像——转置+水平翻转
java·算法·leetcode·职场和发展