Skip to content

BufferedOutputStream:把「多次写」变成「一次写」

写文件和读文件是镜像的。

读的时候,我们希望一次从磁盘读一大块数据到内存。 写的时候,我们希望先把数据攒在内存里,攒够了一大批再一次性写到磁盘。

BufferedOutputStream 就是干这个的。

核心原理

无缓冲时:

java
for (int i = 0; i < 8192; i++) {
    fos.write(data[i]);  // 每次 write 可能触发一次磁盘写入
}
// 写 8192 字节 = 8192 次系统调用

有缓冲时:

java
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("out.dat"));
for (int i = 0; i < 8192; i++) {
    bos.write(data[i]);  // 先写到内存缓冲区
}
// 缓冲区满了才触发一次 write
// 写 8192 字节 = 1 次系统调用

数据先到缓冲区,满了或 close 时才真正写到磁盘。

内部原理

java
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) 先检查缓冲区满不满,不满就往里写,满了就先 flushBuffer() 清空缓冲区,再写新数据。

基本用法

java
// 默认 8KB 缓冲区
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("data.bin"));

// 指定缓冲区大小
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("data.bin"), 16384);

// 典型用法
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("output.dat"))) {
    byte[] data = "Hello World".getBytes();
    bos.write(data);
    bos.flush();  // 主动刷新到磁盘
}

flush() 时机:什么时候需要

场景是否需要 flush()
写文件,正常 close不需要,close() 会自动 flush
写网络流,需要即时发送需要,每写一批就 flush
写日志,需要实时落盘需要,每条日志都 flush
写文件但程序可能崩溃需要,崩溃前先 flush
java
// 日志场景:必须及时 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("app.log"))) {
    for (String log : logs) {
        bos.write((log + "\n").getBytes());
        bos.flush();  // 不 flush,程序崩溃时这行日志就丢了
    }
}

// 普通文件场景:不需要手动 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("data.bin"))) {
    byte[] data = getData();
    bos.write(data);
    // close() 时自动 flush
}

配合 BufferedInputStream 使用

文件拷贝的标准组合:

java
public static void copyFile(String src, String dst) throws IOException {
    try (
        BufferedInputStream in = new BufferedInputStream(
            new FileInputStream(src));
        BufferedOutputStream out = new BufferedOutputStream(
            new FileOutputStream(dst))
    ) {
        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
    }
    // close() 时 out 自动 flush,文件完整写入
}

避坑指南

缓冲区太小

java
// ❌ 缓冲区太小:系统调用次数依然很多
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("out.dat"), 512);  // 512 字节,每 512 字节写一次

// ✅ 合适大小:真正减少系统调用
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("out.dat"), 8192);  // 8KB,每 8KB 写一次

忘记 close()

java
// ❌ 数据还在缓冲区,程序退出了,文件不完整
BufferedOutputStream bos = new BufferedOutputStream(
    new FileOutputStream("out.dat"));
bos.write("data".getBytes());
// 没有 close(),缓冲区数据丢失

// ✅ 用 try-with-resources
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("out.dat"))) {
    bos.write("data".getBytes());
}
// 自动 close() → 自动 flush() → 文件完整

写入口诀:写网络/日志记得 flush,写文件交给 close,缓冲区 8KB 刚刚好。

基于 VitePress 构建