缓冲流 | JavaSE

缓冲流

一、学习目标

完成本章后,你应该能够:

  • 能够解释什么是缓冲流,以及缓冲流为什么能够改善 IO 读写性能。
  • 能够区分 BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter 四种缓冲流。
  • 能够使用字节缓冲流高效复制文件。
  • 能够使用 BufferedReader.readLine() 按行读取文本。
  • 能够使用 BufferedWriter.newLine() 写出跨平台换行。
  • 能够理解“节点流 + 包装流”的 IO 流组合思想。
  • 能够判断什么时候应该使用字节缓冲流,什么时候应该使用字符缓冲流。

二、核心知识

2.1 为什么需要缓冲流

前面已经学习了:

FileInputStream
FileOutputStream

FileReader
FileWriter

它们都可以直接完成文件读写。

那么为什么还需要缓冲流?

核心原因是:

缓冲流通过在内存中设置缓冲区,减少底层 IO 操作的频率,从而改善数据读写性能。

可以先用一个生活中的例子理解。

假设需要搬 1000 本书。

第一种方式:

每次拿 1 本
书架 → 仓库
书架 → 仓库
书架 → 仓库
……

需要往返很多次。

第二种方式:

准备一个箱子
一次装几十本
箱子装满后一起搬走

搬运的数据没有变化,但是:

昂贵的“往返操作”减少了。

缓冲流的思想就是类似的。


2.2 Java 中四种核心缓冲流

Java IO 中主要有四种缓冲流:

| 类型 | 类 | 数据单位 | 作用 | | -------------- | ---------------------- | -------- | ---------------------------- | | 字节缓冲输入流 | BufferedInputStream | byte | 高效读取字节 | | 字节缓冲输出流 | BufferedOutputStream | byte | 高效写出字节 | | 字符缓冲输入流 | BufferedReader | char | 高效读取字符,并支持按行读取 | | 字符缓冲输出流 | BufferedWriter | char | 高效写字符,并支持便捷换行 |

从整个 IO 体系来看:

字节流
├── InputStream
│   └── BufferedInputStream
│
└── OutputStream
    └── BufferedOutputStream


字符流
├── Reader
│   └── BufferedReader
│
└── Writer
    └── BufferedWriter

需要特别注意:

缓冲流通常不是直接取代底层流,而是包装已有 IO 流

例如:

FileInputStream fis = new FileInputStream("a.txt");
BufferedInputStream bis = new BufferedInputStream(fis);

可以理解为:

文件
 ↑
FileInputStream
 ↑
BufferedInputStream
 ↑
程序

程序不再直接操作 FileInputStream,而是通过 BufferedInputStream 进行读取。

这种:

一个流包装另一个流

的设计思想,在 Java IO 中非常重要。


2.3 什么是缓冲区

缓冲流内部维护一块内存区域,用于暂存数据。

教材中经常将默认缓冲区理解为:

8 KiB ≈ 8192 字节/字符规模

这个数字可以帮助理解缓冲思想,但编写业务代码时不应该依赖某个固定的默认缓冲区大小。

真正应该掌握的是:

缓冲流通过批量读取、批量写出降低底层 IO 调用频率。


2.4 缓冲流并不会改变数据类型

这是一个非常重要的判断题。

BufferedInputStream

包装以后是不是变成字符流了?

不是。

它仍然是:

字节输入流

同样:

BufferedReader

仍然是:

字符输入流

所以:

BufferedInputStream  → byte
BufferedOutputStream → byte

BufferedReader       → char
BufferedWriter       → char

“Buffered”只表示:

增加了缓冲能力。

不是改变了数据类型。


三、使用方法

3.1 BufferedInputStream

BufferedInputStream 是:

字节缓冲输入流。

基本构造方式:

BufferedInputStream(InputStream in)

所以通常写成:

InputStream is =
        new BufferedInputStream(
                new FileInputStream("data.txt")
        );

拆开来看就是:

FileInputStream fis = new FileInputStream("data.txt");

BufferedInputStream bis =
        new BufferedInputStream(fis);

关系如下:

文件
 ↓
FileInputStream
 ↓
BufferedInputStream
 ↓
程序

3.2 BufferedOutputStream

BufferedOutputStream 是:

字节缓冲输出流。

构造方式:

BufferedOutputStream(OutputStream out)

典型代码:

OutputStream os =
        new BufferedOutputStream(
                new FileOutputStream("copy.txt")
        );

结构:

程序
 ↓
BufferedOutputStream
 ↓
FileOutputStream
 ↓
文件

3.3 使用字节缓冲流复制文件

复制文件仍然属于:

读 → 写

只不过现在读写双方都加入了缓冲。

import java.io.*;

public class BufferedCopyDemo {
    public static void main(String[] args) throws IOException {

        try (
                InputStream in =
                        new BufferedInputStream(
                                new FileInputStream("source.jpg")
                        );

                OutputStream out =
                        new BufferedOutputStream(
                                new FileOutputStream("target.jpg")
                        )
        ) {

            byte[] buffer = new byte[8192];

            int len;

            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
}

核心逻辑仍然只有:

byte[] buffer = new byte[8192];

int len;

while ((len = in.read(buffer)) != -1) {
    out.write(buffer, 0, len);
}

所以一定不要产生这样的错误理解:

“有了 BufferedInputStream,就不需要 byte[] 了。”

两者解决的是不同层面的问题。


3.4 两层缓冲是什么关系

代码:

BufferedInputStream bis =
        new BufferedInputStream(
                new FileInputStream("a.mp4")
        );

byte[] buffer = new byte[8192];

bis.read(buffer);

这里看起来存在两个 buffer:

BufferedInputStream 内部缓冲区

+

程序自己创建的 byte[]

它们并不冲突。

可以抽象成:

磁盘
 ↓
FileInputStream
 ↓
BufferedInputStream 内部缓冲
 ↓
byte[] 程序缓冲
 ↓
业务代码

BufferedInputStream 负责减少底层读取频率。

而:

byte[] buffer

则负责让你的程序:

一次处理一批数据,而不是每次只处理一个字节。

因此在传统阻塞式文件复制中:

BufferedInputStream
+
BufferedOutputStream
+
byte[]

是一种非常常见的组合。


3.5 BufferedReader

BufferedReader 是:

字符缓冲输入流。

构造器:

BufferedReader(Reader in)

例如:

Reader reader = new FileReader("article.txt");

BufferedReader br =
        new BufferedReader(reader);

也可以直接写:

BufferedReader br =
        new BufferedReader(
                new FileReader("article.txt")
        );

3.6 BufferedReader 最重要的方法:readLine()

BufferedReader 最值得掌握的新增能力是:

String readLine()

作用:

一次读取一行文本。

例如文件:

Java
MySQL
Spring Boot
Vue

代码:

import java.io.*;

public class BufferedReaderDemo {
    public static void main(String[] args) throws IOException {

        try (
                BufferedReader br =
                        new BufferedReader(
                                new FileReader("data.txt")
                        )
        ) {

            String line;

            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

核心模板:

String line;

while ((line = br.readLine()) != null) {
    // 处理这一行
}

这是以后处理文本文件非常常见的代码。


3.7 readLine() 的返回值

假设文件:

hello
java
world

第一次:

br.readLine();

得到:

"hello"

第二次:

"java"

第三次:

"world"

继续读取:

null

因此:

while ((line = br.readLine()) != null)

的意思就是:

只要还能读取到一行,就继续处理。


3.8 readLine() 不包含换行符

这是一个很容易忽略的细节。

文件:

Java\n
MySQL\n
Redis

执行:

String line = br.readLine();

得到的字符串只是:

Java

不会包含:

\n

也就是说:

readLine() 会读取一行内容,但返回结果中不包含行终止符。

这也是为什么我们把文件按行读取后重新写出去时,需要重新执行换行操作。


3.9 BufferedWriter

BufferedWriter 是:

字符缓冲输出流。

构造器:

BufferedWriter(Writer out)

例如:

BufferedWriter bw =
        new BufferedWriter(
                new FileWriter("result.txt")
        );

可以继续使用普通 Writer 已经拥有的:

write()

方法。

例如:

bw.write("Java");
bw.write("MySQL");
bw.write("Redis");

3.10 BufferedWriter 的 newLine()

BufferedWriter 提供了一个非常实用的方法:

newLine()

作用:

写入当前操作系统对应的行分隔符。

例如:

bw.write("Java");
bw.newLine();

bw.write("MySQL");
bw.newLine();

bw.write("Spring Boot");

最终文件:

Java
MySQL
Spring Boot

相比直接写:

bw.write("\n");

newLine() 更能表达:

“我要换行。”

而不是:

“我要写一个特定字符。”

因此在字符缓冲输出流中,应优先认识:

newLine()

3.11 BufferedReader + BufferedWriter

处理文本文件时,两者通常配套出现。

例如复制文本:

import java.io.*;

public class TextCopyDemo {

    public static void main(String[] args) throws IOException {

        try (
                BufferedReader br =
                        new BufferedReader(
                                new FileReader("source.txt")
                        );

                BufferedWriter bw =
                        new BufferedWriter(
                                new FileWriter("target.txt")
                        )
        ) {

            String line;

            while ((line = br.readLine()) != null) {

                bw.write(line);
                bw.newLine();
            }
        }
    }
}

数据流向:

source.txt
   ↓
FileReader
   ↓
BufferedReader
   ↓
String
   ↓
BufferedWriter
   ↓
FileWriter
   ↓
target.txt

这里已经开始体现 Java IO 非常重要的一种思想:

通过组合不同的流,让每一层负责一种能力。


四、原理与进阶

4.1 为什么缓冲流通常更高效

假设:

FileInputStream fis = new FileInputStream("a.mp4");

while (fis.read() != -1) {
}

每次只请求:

1 byte

程序需要进行大量读取调用。

而缓冲输入流大致采用:

程序请求数据
      ↓
缓冲区有数据?
 ┌────┴────┐
 有        没有
 ↓          ↓
直接返回   从底层流批量读取
            ↓
         放入缓冲区
            ↓
         返回数据

因此很多读取请求可以直接从:

内存缓冲区

获得。

这是缓冲思想的核心。

不过需要注意:

“调用一次 read() 就一定访问一次物理磁盘”并不是准确的系统级描述。

操作系统本身也存在页缓存、文件系统缓存等机制。

Java 缓冲流解决的是:

Java IO 调用和数据搬运层面的缓冲问题。

不能简单理解成“每一个 Java read 都对应机械硬盘读一次”。


4.2 装饰器思想

观察:

new BufferedInputStream(
        new FileInputStream("a.txt")
);

FileInputStream 提供:

从文件读取字节

BufferedInputStream 提供:

缓冲能力

两者组合:

文件读取能力
+
缓冲能力

以后还可能看到:

new BufferedReader(
        new InputStreamReader(
                new FileInputStream("a.txt")
        )
);

这里出现三层:

FileInputStream
        ↓
InputStreamReader
        ↓
BufferedReader

分别负责:

FileInputStream
负责:从文件读取 byte

InputStreamReader
负责:byte → char

BufferedReader
负责:缓冲 + 按行读取

这就是 Java IO 中极其重要的:

流包装 / 装饰器式组合思想。

不要只背类名。

真正应该学会的是:

我现在缺什么能力?
        ↓
找到负责这种能力的流
        ↓
把不同流组合起来

4.3 flush() 是什么

输出缓冲流的数据可能暂时保存在缓冲区。

因此存在:

flush()

作用:

把已经缓冲的输出数据向底层输出目标推进。

例如:

bw.write("hello");

bw.flush();

但是正常使用:

close()

关闭输出流时,也会完成必要的刷新。

因此:

正常结束:
close()

程序运行中要求数据立即向下层提交:
flush()

不要形成:

write() 一次就 flush() 一次

这样的习惯。

如果频繁刷新,就会削弱缓冲的意义。


五、实践应用

5.1 文件复制

对于图片、视频、压缩包等:

.jpg
.png
.mp4
.zip
.pdf
.class

应当采用:

字节流

常见组合:

BufferedInputStream
+
BufferedOutputStream
+
byte[]

而不是:

Reader / Writer

5.2 文本逐行处理

如果业务是:

读取日志
读取配置
处理 CSV 文本
处理文章
读取代码文件
逐行统计

那么:

BufferedReader.readLine()

通常非常方便。

例如统计文件行数:

import java.io.*;

public class CountLineDemo {

    public static void main(String[] args) throws IOException {

        try (
                BufferedReader br =
                        new BufferedReader(
                                new FileReader("article.txt")
                        )
        ) {

            int count = 0;

            while (br.readLine() != null) {
                count++;
            }

            System.out.println("总行数:" + count);
        }
    }
}

5.3 按行处理并重新写出

例如源文件:

3. Spring Boot
1. Java
2. MySQL

读取:

BufferedReader

存入集合:

List<String>

排序后:

BufferedWriter

重新写出。

核心结构:

文件
 ↓
BufferedReader
 ↓
List
 ↓
排序/过滤/处理
 ↓
BufferedWriter
 ↓
新文件

这其实已经把前面学习的:

集合
+
Lambda / Stream
+
File
+
IO

逐渐组合起来了。


六、常见问题

6.1 缓冲流是不是只能操作文件?

不是。

例如:

BufferedInputStream

要求的是:

InputStream

而不是:

FileInputStream

所以以后网络编程中也可能出现:

Socket
 ↓
InputStream
 ↓
BufferedInputStream

这就是为什么:

BufferedInputStream(InputStream in)

参数类型设计成父类 InputStream


6.2 有 BufferedInputStream 后还需要 byte[] 吗?

通常仍然建议批量读取。

例如:

byte[] buffer = new byte[8192];

int len;

while ((len = bis.read(buffer)) != -1) {
    bos.write(buffer, 0, len);
}

不要把:

缓冲流

与:

程序中的 byte[]

理解成二选一。


6.3 BufferedReader 能读取图片吗?

从 API 层面可以读取底层字符来源,但:

不应该使用字符流处理图片等二进制文件。

图片应该使用:

InputStream / OutputStream

因为字符流会涉及:

字节 ↔ 字符

的编码转换。

这可能破坏原始二进制数据。


6.4 readLine() 为什么判断 null?

因为:

null

代表:

已经到达输入末尾,没有下一行。

不是:

读取到空行

空行通常得到:

""

所以:

null ≠ ""

一定要分清。


6.5 newLine() 和 "\n" 一样吗?

目标都是换行,但语义不同。

"\n"

显式写入 LF 字符。

而:

newLine()

写入当前平台对应的行分隔符。

因此字符文件处理中:

newLine()

通常更合适。


6.6 close() 应该关闭哪个流?

如果:

BufferedInputStream bis =
        new BufferedInputStream(fis);

通常操作并关闭:

最外层包装流

即可。

关闭包装流会继续关闭其包装的底层流。

例如:

BufferedInputStream.close()
        ↓
FileInputStream.close()

资源释放的完整机制将在后续 07-13 资源释放与 try-with-resources 中系统整理。


七、练习与验收

7.1 知识问答

  • [ ] Java 中四种主要缓冲流分别是什么?
  • [ ] 字节缓冲流与字符缓冲流分别处理什么数据?
  • [ ] 缓冲流为什么通常可以改善 IO 性能?
  • [ ] BufferedInputStream 会不会把字节流变成字符流?
  • [ ] BufferedReader 最重要的新增方法是什么?
  • [ ] readLine() 返回的字符串是否包含换行符?
  • [ ] readLine() 返回 null 表示什么?
  • [ ] BufferedWriter 新增的便捷方法是什么?
  • [ ] flush()close() 有什么区别?
  • [ ] 为什么不能认为每一次 FileInputStream.read() 都一定直接访问一次物理磁盘?
  • [ ] 为什么缓冲流体现了 IO 流的包装思想?
  • [ ] 为什么复制图片和视频仍然应该使用字节流?

7.2 代码阅读

阅读代码,不运行:

try (
        BufferedReader br =
                new BufferedReader(
                        new FileReader("a.txt")
                )
) {

    String line;

    while ((line = br.readLine()) != null) {
        System.out.print(line);
    }
}

回答:

  • [ ] 如果 a.txt 中有三行文本,最终控制台一定显示三行吗?
  • [ ] 为什么?
  • [ ] 应该怎样修改?

阅读:

try (
        BufferedInputStream bis =
                new BufferedInputStream(
                        new FileInputStream("a.jpg")
                )
) {

    byte[] buffer = new byte[1024];

    int len;

    while ((len = bis.read(buffer)) != -1) {
        System.out.println(len);
    }
}

回答:

  • [ ] len 表示什么?
  • [ ] 最后一次读取时 len 一定是 1024 吗?
  • [ ] -1 表示什么?

7.3 手写代码

关闭 AI 自动补全,独立完成:

  • [ ] 使用 BufferedInputStreamBufferedOutputStream 复制一张图片。
  • [ ] 使用字节数组一次读取多个字节。
  • [ ] 使用 BufferedReader 按行读取一个 .txt 文件。
  • [ ] 使用 BufferedWriter 写出 5 行学生信息。
  • [ ] 使用 readLine() 统计文本文件总行数。
  • [ ] 使用缓冲字符流将一个文本文件逐行复制到另一个文件。

7.4 Debug

下面代码存在问题:

BufferedReader br =
        new BufferedReader(
                new FileReader("data.txt")
        );

String line;

while ((line = br.readLine()) != null) {
    System.out.print(line);
}

要求:

  • [ ] 找出最终文本格式可能发生的变化。
  • [ ] 说明原因。
  • [ ] 修复代码。

下面代码:

BufferedOutputStream bos =
        new BufferedOutputStream(
                new FileOutputStream("a.txt")
        );

bos.write("hello".getBytes());

要求:

  • [ ] 分析程序结束时为什么不能养成“不关闭资源”的习惯。
  • [ ] 给出合理的资源管理结构。
  • [ ] 说明 flush()close() 的关系。

7.5 综合训练

设计一个程序:

源文件:students.txt

文件中每行表示一个学生:

1003,王五,85
1001,张三,93
1002,李四,78

要求:

读取文件
→ 按行处理
→ 存入集合
→ 按成绩排序
→ 写入 students_sorted.txt

限制:

  • 输入必须使用 BufferedReader
  • 输出必须使用 BufferedWriter
  • 必须保留一条学生记录占一行
  • 不允许一次把整个文件读成一个字符串

先完成:

数据流设计
→ 类/数据模型设计
→ 伪代码
→ Java 实现

7.6 本章验收

不查看资料,能够独立写出:

BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter

四种流的创建方式。

能够解释:

缓冲流解决什么问题
        ↓
为什么提高性能
        ↓
readLine() 有什么价值
        ↓
newLine() 有什么价值
        ↓
字节缓冲流和字符缓冲流怎么选择

最后能够独立完成:

使用字节缓冲流复制任意二进制文件,并使用字符缓冲流逐行处理文本文件。

达到以上标准,本章核心知识即掌握。