缓冲流与包装流
概述
缓冲流在底层流(如 字节流、字符流)之上增加内存缓冲区,减少与磁盘或网络的直接读写次数,从而显著提升 IO 性能。包装流(也称装饰器流)是指「接收另一个流作为参数、在其上增加某种能力」的流;缓冲流就是典型的包装流——它们包装 InputStream/OutputStream 或 Reader/Writer,为其增加缓冲与便捷方法(如 BufferedReader.readLine())。
理解缓冲流与包装流,是写出高效、可维护 IO 代码的基础,也是学习 NIO 中 Buffer 概念的前置。
缓冲流与包装流体系概览
| 类型 | 常用类 | 包装对象 | 说明 |
|---|---|---|---|
| 字节缓冲流 | BufferedInputStream | InputStream | 为字节输入流增加缓冲,减少 read() 系统调用 |
BufferedOutputStream | OutputStream | 为字节输出流增加缓冲,批量写入底层流 | |
| 字符缓冲流 | BufferedReader | Reader | 为字符输入流增加缓冲,并提供 readLine() 按行读取 |
BufferedWriter | Writer | 为字符输出流增加缓冲,并提供 newLine() 换行 |
- 包装关系:缓冲流通过构造方法接收「底层流」,例如
new BufferedInputStream(new FileInputStream("a.txt"))。关闭缓冲流时,会关闭其包装的底层流,因此只需关闭最外层流。 - 默认缓冲区大小:通常为 8KB(8192 字节/字符),也可在构造时指定,如
new BufferedInputStream(in, 16384)。
提示
多数文件读写场景下,应优先使用缓冲流包装 FileInputStream/FileOutputStream 或 FileReader/FileWriter,可明显减少 IO 次数,尤其在循环中逐字节/逐字符读写时。
基本用法
BufferedInputStream / BufferedOutputStream
- 构造:
BufferedInputStream(InputStream in)、BufferedInputStream(InputStream in, int size);BufferedOutputStream(OutputStream out)、BufferedOutputStream(OutputStream out, int size)。 - 读写:与底层
InputStream/OutputStream的read(byte[])、write(byte[])用法一致;数据先经过内部缓冲区,再与底层流交互。 - flush:
BufferedOutputStream.flush()将缓冲区内容写入底层流;BufferedInputStream无flush。 - close:关闭时会关闭包装的底层流,并释放缓冲区。
BufferedReader / BufferedWriter
- 构造:
BufferedReader(Reader in)、BufferedReader(Reader in, int size);BufferedWriter(Writer out)、BufferedWriter(Writer out, int size)。 - 读写:与底层
Reader/Writer的read(char[])、write(String)等用法一致;另提供:String readLine():读取一行(不包含换行符),末尾返回null。void newLine():写出与系统无关的换行符。
- flush:
BufferedWriter.flush()将缓冲区内容写入底层流;写入后若需立即落盘,应调用flush()。 - close:关闭时会先 flush(若有未写出数据),再关闭包装的底层流。
说明
缓冲流的 read、write、close 会抛出受检异常 IOException,必须用 try-catch 捕获或在方法上 throws IOException。推荐用 try-with-resources 管理流,只需关闭最外层缓冲流即可。详见 异常处理。
使用示例
示例 1:用 BufferedInputStream 包装 FileInputStream 读取文件
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class BufferedReadFile {
public static void main(String[] args) {
// 缓冲流包装文件流;try-with-resources 只关闭 BufferedInputStream,其会关闭内部 FileInputStream
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream("data/hello.txt"))) {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
// 实际读到的字节数为 len,可能小于 buf.length
System.out.print(new String(buf, 0, len));
}
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
}示例 2:用 BufferedWriter 包装 FileWriter 写入并 flush
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
public class BufferedWriteFile {
public static void main(String[] args) {
try (BufferedWriter out = new BufferedWriter(new FileWriter("data/output.txt"))) {
out.write("第一行");
out.newLine(); // 与系统无关的换行
out.write("第二行");
out.flush(); // 确保写入缓冲区的内容立即写到文件(try-with-resources 关闭前也会 flush)
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
}示例 3:BufferedReader 按行读取(readLine)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class ReadByLine {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("data/hello.txt"))) {
String line;
int lineNum = 0;
while ((line = reader.readLine()) != null) {
lineNum++;
System.out.println(lineNum + ": " + line);
}
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
}
}提示
BufferedReader.readLine() 是处理文本文件按行读取最常用的方式;返回的字符串不包含换行符。若需指定编码,可包装 InputStreamReader:new BufferedReader(new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))。
示例 4:多层包装与关闭顺序
import java.io.*;
import java.nio.charset.StandardCharsets;
public class LayeredStreams {
public static void main(String[] args) {
// 从内到外:FileInputStream → InputStreamReader(字节转字符,指定 UTF-8)→ BufferedReader
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("data/hello.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("读取失败: " + e.getMessage());
}
// 只需关闭最外层的 BufferedReader;其 close() 会依次关闭 InputStreamReader、FileInputStream
}
}包装流(装饰器)概念
Java IO 中大量使用装饰器模式:用一个流「包装」另一个流,在不修改原有类的前提下增加缓冲、编码转换、行读写等能力。
| 概念 | 说明 |
|---|---|
| 底层流 | 直接与数据源/目标打交道,如 FileInputStream、FileWriter |
| 包装流 | 接收一个已有流,在其上增加行为,如 BufferedInputStream、InputStreamReader |
| 关闭顺序 | 只需关闭最外层包装流;其 close() 会委托给内层流,最终关闭底层流 |
| flush | 对 BufferedOutputStream、BufferedWriter 等有缓冲的输出流,flush() 将缓冲区内容推到下层;关闭时一般会先 flush |
注意
不要重复关闭已被包装的底层流——例如不要把 FileInputStream 和包装它的 BufferedInputStream 都放进 try-with-resources 并分别关闭,只关闭最外层的 BufferedInputStream 即可,否则可能导致「流已关闭」的异常或重复关闭。
注意事项
- 何时使用缓冲流:对文件、网络等「每次 read/write 成本较高」的流,建议用缓冲流包装;对
ByteArrayInputStream等已在内存中的流,包装缓冲流收益较小。 - 输出流记得 flush:在需要立即将数据写入磁盘或网络时(如日志、协议报文),在
close()前可显式调用flush();使用 try-with-resources 时,close()会先 flush 再关闭。 - readLine 与编码:
BufferedReader的readLine()依赖底层Reader的编码;若需指定 UTF-8 等,用InputStreamReader包装FileInputStream并传入Charset,再包装为BufferedReader。 - 缓冲区大小:默认 8KB 对多数场景已足够;大文件或高吞吐时可适当增大,如
new BufferedInputStream(in, 65536)。
注意
不要在多线程中共享同一个流实例(包括缓冲流);流通常不是线程安全的,多线程读写应使用各自独立的流或使用线程安全的 API。
相关链接
- 字节流 — 底层字节流 InputStream/OutputStream
- 字符流 — 底层字符流 Reader/Writer 与编码
- 异常处理 — try-with-resources 与受检异常
- NIO 简介 — Channel 与 Buffer 的现代 IO 方式
- Oracle Java 教程 - Buffered I/O