乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

缓冲流

小崔测试完两种文件复制方案后感叹:"一个8KB的字节数组就让速度提升了40多倍!"
李眉:"这就是缓冲的力量。Java已经把这种缓冲逻辑封装成了现成的类——BufferedInputStream 和 BufferedReader,你不用自己管理缓冲区数组,直接享受性能红利。"


一、缓冲流定位

缓冲流是典型的处理流(装饰器),它在内部维护一个缓冲区(默认8KB),将多次小的读/写操作合并为少量大的物理操作,从而大幅减少系统调用次数。

四种缓冲流速查

缓冲流对应基类缓冲区默认大小特有方法
BufferedInputStreamInputStream8192 bytes (8KB)无特别额外方法
BufferedOutputStreamOutputStream8192 bytes (8KB)flush()
BufferedReaderReader8192 chars (16KB)readLine() 按行读取
BufferedWriterWriter8192 chars (16KB)newLine() 写出换行符

二、核心类定义与构造方法

2.1 BufferedInputStream / BufferedOutputStream

构造方法说明
BufferedInputStream(InputStream in)默认8KB缓冲区
BufferedInputStream(InputStream in, int size)指定缓冲区大小(字节)
BufferedOutputStream(OutputStream out)默认8KB缓冲区
BufferedOutputStream(OutputStream out, int size)指定缓冲区大小(字节)

2.2 BufferedReader / BufferedWriter

构造方法说明
BufferedReader(Reader in)默认8KB字符缓冲区
BufferedReader(Reader in, int sz)指定缓冲区大小(字符)
BufferedWriter(Writer out)默认8KB字符缓冲区
BufferedWriter(Writer out, int sz)指定缓冲区大小(字符)

2.3 特有方法

方法所属类返回值说明
String readLine()BufferedReader一行字符串(不含换行符),流结束返回null按行读取,极大简化文本处理
void newLine()BufferedWritervoid写出平台相关的换行符(Windows=\r\n, Linux=\n)
void flush()BufferedOutputStream / BufferedWritervoid强制将缓冲区数据写出到被装饰的流

三、缓冲原理深度分析

3.1 BufferedReader 的 readLine() 实现原理

关键:readLine() 返回的字符串不包含换行符。写入时如果需要换行,使用 newLine() 而不是手动写 \n。

3.2 BufferedOutputStream 的缓冲写入机制

// BufferedOutputStream 核心逻辑(简化版)
public class BufferedOutputStream extends FilterOutputStream {
    protected byte[] buf;          // 内部缓冲区
    protected int count;           // 当前已缓冲的字节数

    public synchronized void write(int b) throws IOException {
        if (count >= buf.length) {
            flushBuffer();  // 缓冲区满,批量写出到被装饰的流
        }
        buf[count++] = (byte) b;  // 只写到内部数组,不触发物理写入
    }

    public synchronized void flush() throws IOException {
        flushBuffer();     // 强制写出
        out.flush();       // 再flush底层流
    }

    private void flushBuffer() throws IOException {
        if (count > 0) {
            out.write(buf, 0, count);  // 一次性批量写出
            count = 0;
        }
    }
}

write(int b) 不是每次触发系统调用——它只把字节放进内部数组。只有当数组满了,或者调用 flush()/close(),才真正写出。

3.3 缓冲区大小对性能的影响

缓冲区大小1MB文件系统调用次数(读)典型耗时内存开销
无缓冲(逐字节)1,048,576~180 ms0
4KB256~5 ms4KB
8KB(默认)128~3 ms8KB
64KB16~2 ms64KB
1MB1~2 ms1MB

经验法则:默认8KB对大多数场景足够。特殊场景(如SSD上的大文件读写)可适当增大到64KB。过大(如10MB)浪费内存且边际收益接近于零。


四、完整示例

示例一:飞翔科技日志文件按行读取分析

场景:飞翔科技运维团队需要分析系统日志 system.log,统计 ERROR 级别的日志数量并提取所有错误行。

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * 飞翔科技 - 日志分析工具
 * 使用 BufferedReader.readLine() 按行读取日志
 */
public class LogAnalyzer {
    public static void main(String[] args) {
        String logPath = "C:/feixiang/logs/system.log";
        List<String> errorLines = new ArrayList<>();
        int totalLines = 0;
        int errorCount = 0;
        int warnCount = 0;
        int infoCount = 0;

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream(logPath), "UTF-8"))) {

            String line;
            while ((line = br.readLine()) != null) {
                totalLines++;
                if (line.contains("[ERROR]")) {
                    errorCount++;
                    errorLines.add(line);
                } else if (line.contains("[WARN]")) {
                    warnCount++;
                } else if (line.contains("[INFO]")) {
                    infoCount++;
                }
            }

        } catch (IOException e) {
            System.err.println("日志分析失败:" + e.getMessage());
            return;
        }

        // 输出统计报告
        System.out.println("=== 飞翔科技系统日志分析报告 ===");
        System.out.println("日志文件路径:" + logPath);
        System.out.println("总行数:" + totalLines);
        System.out.println("--------------------------------");
        System.out.printf("INFO  : %d 条%n", infoCount);
        System.out.printf("WARN  : %d 条%n", warnCount);
        System.out.printf("ERROR : %d 条%n", errorCount);
        System.out.println("--------------------------------");

        if (!errorLines.isEmpty()) {
            System.out.println("\n错误日志详情:");
            for (int i = 0; i < errorLines.size(); i++) {
                System.out.printf("  #%d: %s%n", (i + 1), errorLines.get(i));
            }
        }
    }
}
运行输出:
=== 飞翔科技系统日志分析报告 ===
日志文件路径:C:/feixiang/logs/system.log
总行数:1523
--------------------------------
INFO  : 1420 条
WARN  : 87 条
ERROR : 16 条
--------------------------------

错误日志详情:
  #1: [2025-01-15 09:30:13] [ERROR] 数据库连接超时 - 主机:192.168.1.200:3306
  #2: [2025-01-15 10:22:45] [ERROR] 用户认证失败 - 用户ID:10582
  ...

示例二:飞翔科技员工CSV文件读写(带缓冲)

场景:小崔需要读取员工CSV文件,筛选出特定部门的员工,写入新文件。

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * 飞翔科技 - 部门员工筛选工具
 * 使用 BufferedReader/BufferedWriter 高效处理CSV文件
 */
public class EmployeeFilter {

    static class Employee {
        String id;
        String name;
        String department;
        int age;

        Employee(String id, String name, String department, int age) {
            this.id = id;
            this.name = name;
            this.department = department;
            this.age = age;
        }

        String toCsv() {
            return String.join(",", id, name, department, String.valueOf(age));
        }
    }

    public static void main(String[] args) {
        String inputPath = "C:/feixiang/data/employees.csv";
        String outputPath = "C:/feixiang/data/tech_dept_employees.csv";

        List<Employee> techEmployees = new ArrayList<>();

        // 第一步:使用 BufferedReader 读取并筛选
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream(inputPath), StandardCharsets.UTF_8))) {

            // 读取并跳过表头
            String header = br.readLine();
            if (header == null) {
                System.err.println("CSV文件为空!");
                return;
            }

            String line;
            while ((line = br.readLine()) != null) {
                String[] parts = line.split(",");
                if (parts.length >= 4) {
                    String dept = parts[2].trim();
                    // 筛选技术部员工
                    if ("技术部".equals(dept) || "技术总监".equals(dept)) {
                        techEmployees.add(new Employee(
                            parts[0].trim(),
                            parts[1].trim(),
                            dept,
                            Integer.parseInt(parts[3].trim())
                        ));
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("CSV读取失败:" + e.getMessage());
            return;
        }

        // 第二步:使用 BufferedWriter 写出筛选结果
        try (BufferedWriter bw = new BufferedWriter(
                new OutputStreamWriter(
                    new FileOutputStream(outputPath), StandardCharsets.UTF_8))) {

            // 写表头
            bw.write("员工ID,姓名,部门,年龄");
            bw.newLine();

            // 写数据行
            for (Employee emp : techEmployees) {
                bw.write(emp.toCsv());
                bw.newLine();
            }

            System.out.println("=== 筛选结果 ===");
            System.out.println("技术部员工共 " + techEmployees.size() + " 人");
            System.out.println("已输出到:" + outputPath);
            System.out.println("\n技术部员工列表:");
            for (Employee emp : techEmployees) {
                System.out.printf("  %s | %s | %s | %d岁%n",
                    emp.id, emp.name, emp.department, emp.age);
            }

        } catch (IOException e) {
            System.err.println("CSV写入失败:" + e.getMessage());
        }
    }
}

假设 employees.csv:

员工ID,姓名,部门,年龄
E001,大翔,技术经理,38
E002,白歌,技术部,32
E003,小崔,技术部,24
E004,李眉,技术部,29
E005,小张,市场部,27
E006,小王,财务部,31
运行输出:
=== 筛选结果 ===
技术部员工共 4 人
已输出到:C:/feixiang/data/tech_dept_employees.csv

技术部员工列表:
  E001 | 大翔 | 技术经理 | 38岁
  E002 | 白歌 | 技术部 | 32岁
  E003 | 小崔 | 技术部 | 24岁
  E004 | 李眉 | 技术部 | 29岁

五、flush() 方法深度解析

情况数据是否落盘
写入8KB,缓冲区8KB满是(自动触发)
写入1KB,未flush,未close否(数据在内存中!)
写入1KB,调用flush()是
写入1KB,调用close()是(close内部调用flush)

重要:BufferedWriter.flush() 只负责把 Writer 缓冲区刷到底层流。如果底层是 BufferedOutputStream,还需要再 flush 一次。使用 try-with-resources 时,close() 按逆序调用,各层都会自动 flush。


六、易错场景

反例一:忘记关闭缓冲输出流导致文件内容不完整

// ❌ 错误:程序退出前未关闭流,缓冲区数据丢失
BufferedWriter bw = new BufferedWriter(new FileWriter("report.txt"));
bw.write("飞翔科技月度报告");
bw.write("本期营收:500万元");
// 没有 bw.close() —— 文件可能为空或内容不完整!
System.exit(0);

纠正:

// ✅ 正确:try-with-resources 确保 close()
try (BufferedWriter bw = new BufferedWriter(new FileWriter("report.txt"))) {
    bw.write("飞翔科技月度报告");
    bw.newLine();
    bw.write("本期营收:500万元");
} // 自动 close → flush → 数据完整落盘

反例二:readLine() 返回的字符串不包含换行符,写出时忘记补

// ❌ 错误:读出后直接写回,换行符丢失,所有行挤在一行
try (BufferedReader br = ...; BufferedWriter bw = ...) {
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);  // 忘记 bw.newLine();
    }
}

纠正:

// ✅ 正确:使用 newLine() 补充换行符
try (BufferedReader br = ...; BufferedWriter bw = ...) {
    String line;
    while ((line = br.readLine()) != null) {
        bw.write(line);
        bw.newLine();  // 补充平台相关的换行符
    }
}

反例三:BufferedReader 和 BufferedInputStream 用混

// ❌ 错误:BufferedReader 需要 Reader 参数,不接收 InputStream
BufferedReader br = new BufferedReader(new FileInputStream("data.txt"));
// 编译错误!BufferedReader 的构造方法是 BufferedReader(Reader in)

纠正:

// ✅ 正确:通过 InputStreamReader 搭桥
BufferedReader br = new BufferedReader(
    new InputStreamReader(new FileInputStream("data.txt"), "UTF-8"));

七、面试考点

Q1:BufferedReader 的 readLine() 如何判断文件结束?

返回 null 表示文件已读完。注意与 read() 返回 -1 区分——read() 是单个字符读取,用 -1;readLine() 是按行读取,用 null。典型循环:

String line;
while ((line = br.readLine()) != null) {
    // 处理 line
}

Q2:BufferedWriter 写入后一定要 flush() 吗?

不一定每次都需要,但取决于数据可靠性要求。如果使用 try-with-resources,close() 会自动 flush(),无需手动调用。但如果是持续写入的日志文件(流不关闭),则需要定期 flush() 确保数据落盘。也可以构造时指定较小的缓冲区,使其更频繁地自动刷新。

Q3:BufferedInputStream 为什么没有 readLine() 方法?

readLine() 是按字符行读取,需要识别换行符(\n、\r\n),这涉及字符解码。BufferedInputStream 操作的是字节,不知道字节序列在编码后对应的字符边界在哪里,无法可靠地识别"行"。BufferedReader 操作的是解码后的字符,可以安全地扫描换行符。

Q4:缓冲区大小设置为多少最合适?

默认 8KB 适合绝大多数场景。选择依据:① 磁盘块大小:通常 4KB 或 8KB,缓冲区为此值的整数倍效率最高;② 读写模式:顺序大文件操作用较大缓冲区(64KB~256KB),随机小文件操作用默认即可;③ 内存敏感场景:嵌入式或移动设备用较小缓冲区(1KB~4KB)。

Q5:close() 和 flush() 的区别是什么?

flush() 只将缓冲区数据强制写出到目的地,流仍然可用;close() 会先调用 flush(),然后释放底层操作系统资源(文件句柄、网络连接等),此后流不可再用。对于需要长时间打开的文件(如日志),应定期 flush() 而不是频繁 close() 再重新打开。


李眉:"缓冲流是性能优化的利器,几乎任何IO操作都应该加上缓冲。但有一个问题没解决——字节流和字符流之间怎么互转?下一节我们看转换流这个'桥梁'。"

上一页
字符流
下一页
转换流 详解