20260807-笔记.md 44 KB

20260807 课堂笔记 — IO 字节缓冲流(BufferedOutputStream 输出 / BufferedInputStream 输入 + 内部缓冲区与 flush 刷新 + 基础流 vs 扩展流)+ 接口化文件复制(CopyFile 接口 + Copy01 普通字节流实现 / BufferedCopy 缓冲流实现 + System.currentTimeMillis 耗时对比评测)+ 字符流入门(字符集编码表 + Reader/Writer 抽象父类 + FileReader/FileWriter 构造与读写方法)+ 字符缓冲流(BufferedReader / BufferedWriter 包装字符流 + newLine() / readLine() 特有方法)+ IO 流体系小结(基础流 vs 处理流-缓冲流完整分类)+ 随堂练习参考解答(BufferedWriteReadTest 缓冲流写读统计 / ScoreSort 字符缓冲流读取排序写回)

  • 日期:2026-08-07
  • 项目:c260807
  • 包路径:course
  • 作者:WanJL

目录

  1. 接口化设计:CopyFile 接口 —— 统一文件复制的规范
  2. 字节缓冲流概念:BufferedOutputStream / BufferedInputStream(基础流 vs 扩展流)
  3. Copy01 —— 普通字节流复制实现(byte[] 数组缓冲区 + try-catch-finally 关闭资源)
  4. BufferedCopy —— 缓冲流复制实现(缓冲流包装底层字节流)
  5. BufferedStreamTest —— 缓冲流写入 100 万字符(flush 刷新)与读取演示
  6. Demo01 —— Scanner 交互式写入(输入一行写一行)
  7. MyTest —— 耗时对比评测(接口多态 + System.currentTimeMillis)
  8. 字符流入门:字符集编码表 + Reader/Writer + FileReader/FileWriter(Demo02)
  9. 字符缓冲流:BufferedReader / BufferedWriter(Demo03)
  10. IO 流体系小结:基础流 vs 处理流-缓冲流(Demo04)
  11. 随堂练习参考解答:BufferedWriteReadTest / ScoreSort —— 缓冲流写读统计 + 字符缓冲流读取排序写回
  12. 知识点全景总结
  13. 随堂练习要点
  14. 拓展阅读

1. 接口化设计:CopyFile 接口 —— 统一文件复制的规范

概念 —— 接口作为"文件复制"的契约

在文件复制的场景中,先定义一个 CopyFile 接口(复制器接口),抽象出"复制文件"这个方法规范:传入原路径 src 与新路径 desc,返回一个复制结果提示字符串,并声明抛 IOException(因为实现类创建流、读写文件都可能抛编译期异常)。

// 来源:course/CopyFile.java
package course;

public interface CopyFile {
    /**
     * 复制文件的方法
     * @param src 原路径
     * @param desc 新路径
     * @return 复制结果提示
     */
    String copy(String src, String desc) throws IOException;
}

要点:

  • 接口方法默认是 public abstract,实现类必须用 public 重写
  • 方法声明 throws IOException:接口层面就声明了"此操作可能抛 IOException",实现类可以继续 throws,也可以 try-catch 处理后不再抛出
  • 本日核心设计思想:面向接口编程 —— 调用方(MyTest)只依赖 CopyFile 接口类型,具体用哪种复制实现可以随时替换(多态)

2. 字节缓冲流概念:BufferedOutputStream / BufferedInputStream(基础流 vs 扩展流)

概念 —— 什么是字节缓冲流

// 来源:course/Demo01.java(注释部分)
/*
    字节缓冲流
        BufferedOutputStream:这个类是实现缓冲输出流的类,通过设置这样的输出流,程序可以向底层输出流写入字节,
        也就是说它针对的不是真正的文件,而是文件的输出流。
        BufferedInputStream:这个类是实现缓冲输入流的类。通过设置输入流,可以向底层输入流读取数据。
        这个类里面维护了一个字节缓冲区,内部缓冲区会根据需要,从包含的输入流中获取数据填充到缓冲区。一次可以读取多个字节。

        public BufferedOutputStream(OutputStream out)   缓冲字节输出流的构造方法,参数是字节输出流
        public BufferedInputStream(InputStream out)     缓冲字节输入流的构造方法,参数是字节输入流
*/

关键概念 —— 包装流 / 装饰流

  • BufferedOutputStream(缓冲字节输出流):针对的不是真正的文件,而是"文件的输出流"——它把 FileOutputStream 包装起来,先写入内部缓冲区,满了再统一刷到磁盘
  • BufferedInputStream(缓冲字节输入流):内部维护一个字节缓冲区,一次从底层输入流读取多个字节填充缓冲区,后续的 read() 直接从内存缓冲区取,减少与磁盘的交互次数
  • 构造方法:参数是底层字节流(OutputStream / InputStream),也就是先创建 FileOutputStream / FileInputStream,再用缓冲流把它们包装起来
  • 默认缓冲区大小:8192 字节(8KB)

要点:缓冲流本质是"包装流 / 装饰流"——它不直接操作文件,而是给已存在的字节流"加一层缓冲",牺牲少量内存换取大幅减少磁盘 I/O 次数,从而显著提升读写效率。这正对上节课"byte[] 缓冲区循环复制"的思想:缓冲 = 一次操作一批数据,而非一个字节一个字节。

补充概念 —— 基础流 vs 扩展流(20260807 代码同步更新)

// 来源:course/Demo01.java(注释部分,本次更新新增)
/*
    FileInputStream和FileOutputStream属于基础流,他们是直接接触和操作文件的。
    BufferedOutputStream和BufferedInputStream属于扩展流,他们是用来操作基础流的。
*/

要点:

  • 基础流(FileInputStream / FileOutputStream 等)直接接触和操作文件,是数据的"源头与终点"
  • 扩展流(BufferedOutputStream / BufferedInputStream 等)不直接操作文件,而是用来"操作基础流"的——在基础流之上叠加缓冲、转换等增强能力
  • 这个"基础流 + 扩展流"的分工,正是后续所有高级流(字符流、转换流、对象流)的共同组织方式:先建基础流,再用扩展流包装它

3. Copy01 —— 普通字节流复制实现(byte[] 数组缓冲区 + try-catch-finally 关闭资源)

实现 —— 实现 CopyFile 接口

Copy01 implements CopyFile,在 copy() 方法内用 FileInputStream 读 + FileOutputStream 写 + byte[8192] 数组缓冲区循环复制,并用 try-catch-finally 保证资源一定关闭:

// 来源:course/Copy01.java
public class Copy01 implements CopyFile {
    @Override
    public String copy(String src, String desc) {
        FileOutputStream fos = null;
        FileInputStream fis = null;
        try {
            fos = new FileOutputStream(desc);
            fis = new FileInputStream(src);
            byte[] bytes = new byte[8192];
            int byteCount;
            while ((byteCount = fis.read(bytes)) != -1) {   // 读一批(-1 表示读完)
                fos.write(bytes, 0, byteCount);             // 只写本次有效字节
            }
        } catch (IOException e) {
            return "文件复制失败";                          // 复制失败返回提示
        } finally {
            try {
                fos.close();                                // finally 中关闭资源
                fis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);              // 关闭失败包装成运行时异常
            }
        }
        return "文件复制成功";
    }
}

要点:

  • 流变量在 try 外面声明为 null,因为 finally 块要访问它们(作用域问题)
  • try-catch-finally:finally 保证无论是否发生异常都会执行 close();catch (IOException) 捕获复制失败返回提示信息
  • IOException 包装成 RuntimeException:finally 里再抛的 IOException 属于编译期异常,用运行时异常包装后可以不强制上层处理
  • 该实现没有把异常抛给调用方(接口声明了 throws,实现类可以选择 try-catch 消化掉),属于"方法内部自处理"风格

4. BufferedCopy —— 缓冲流复制实现(缓冲流包装底层字节流)

实现 —— 用缓冲流替代普通数组缓冲区

BufferedCopy 同样实现 CopyFile 接口,但用 BufferedInputStream / BufferedOutputStream 包装底层文件流,再用 byte[8192] 数组作为"二次缓冲":

// 来源:course/BufferedCopy.java
public class BufferedCopy implements CopyFile {
    @Override
    public String copy(String src, String desc) throws IOException {
        //1、创建文件字节输出流和缓冲流对象
        FileOutputStream fos = new FileOutputStream(desc);
        BufferedOutputStream bos = new BufferedOutputStream(fos);   // 缓冲输出流包装文件输出流
        //2、创建文件字节输入流和缓冲流对象
        FileInputStream fis = new FileInputStream(src);
        BufferedInputStream bis = new BufferedInputStream(fis);     // 缓冲输入流包装文件输入流

        byte[] bytes = new byte[8192];
        int count;
        while ((count = bis.read(bytes)) != -1) {
            bos.write(bytes, 0, count);
        }
        bis.close();
        fis.close();
        bos.close();
        fos.close();
        return "复制成功";
    }
}

两种实现的对比

对比维度 Copy01(普通字节流) BufferedCopy(缓冲流)
核心流 FileInputStream + FileOutputStream 缓冲流包装 FileInputStream / FileOutputStream
缓冲区 手动 byte[8192] 数组 缓冲流内部 8KB 缓冲区 + 手动数组(双缓冲)
异常处理 try-catch-finally 内部处理,返回提示 直接 throws IOException 交给调用方
本质优化 减少磁盘交互次数 缓冲流再叠一层缓冲,进一步减少磁盘 I/O

要点:

  • 包装流程固定:先建底层文件流 → 再建缓冲流包装它 → 操作缓冲流 → 关闭
  • 关闭顺序:先关缓冲流(bis / bos)再关底层流(fis / fos);实际上 close() 缓冲流会自动关闭它包装的底层流,手写两层 close 更保险
  • 该实现选择 throws IOException(接口声明抛异常),与 Copy01 的"内部消化"形成对照——接口允许两种实现风格并存

5. BufferedStreamTest —— 缓冲流写入 100 万字符(flush 刷新)与读取演示

写端 —— 100 万随机大写字母 + 每 500 个换行并 flush

// 来源:course/BufferedStreamTest.java(写端,已注释保留)
FileOutputStream fos = new FileOutputStream("D:/abc.txt");
BufferedOutputStream bos = new BufferedOutputStream(fos);   // 缓冲流包装文件输出流
Random ran = new Random();
for (int i = 0; i < 1000000; i++) {
    int c = ran.nextInt(26) + 65;         // 随机生成大写字母 A~Z(65~90)
    bos.write(c);                          // 先写入内部缓冲区
    if (i % 500 == 0) {
        bos.write("\r\n".getBytes());      // 每 500 个字母写一个换行
        bos.flush();                       // 手动把缓冲区数据刷到磁盘
    }
}
bos.close();
fos.close();

读端 —— 缓冲流逐字节读取显示

// 来源:course/BufferedStreamTest.java(读端,活动代码)
FileInputStream fis = new FileInputStream("D:/abc.txt");
BufferedInputStream bis = new BufferedInputStream(fis);     // 缓冲流包装文件输入流
int b;
while ((b = bis.read()) != -1) {
    System.out.print((char) b);            // 读到的字节强转成字符输出
}
bis.close();
fis.close();

要点:

  • flush() 刷新缓冲区:缓冲流默认缓冲区满了才自动写入磁盘,flush() 可以手动强制把当前缓冲区数据刷到磁盘(这里每 500 个字符刷一次,保证数据及时落盘)
  • read() 逐字节读也很快:虽然 bis.read() 每次只返回一个字节,但数据预先成批读入内部缓冲区,逐字节读取实际上是从内存缓冲区取,性能远高于无缓冲的逐字节磁盘读取
  • (char) b 强转:读取到的是 int(字节值),转成 char 才能显示为字母
  • Random 随机字母:ran.nextInt(26) + 65 生成 65~90 的 ASCII 码,对应 'A'~'Z'

6. Demo01 —— Scanner 交互式写入(输入一行写一行)

实现 —— 控制台输入即写入文件

// 来源:course/Demo01.java
FileOutputStream fos = new FileOutputStream("D:abc.txt");
Scanner sc = new Scanner(System.in);
while (true) {
    System.out.println("请写入内容:");
    String s = sc.nextLine();           // 读取一行输入
    fos.write(s.getBytes());            // 字符串转字节数组写入文件
}

要点:

  • Scanner + IO 结合:键盘输入 → getBytes() 转字节数组 → fos.write() 写入文件,形成"控制台 → 文件"的数据流动
  • 无限 while(true) 循环持续写入,直到强制终止程序(Ctrl+C)
  • ⚠️ 注意:这里没有 close() 关闭流——这是教学演示,实际开发中流必须关闭,否则文件句柄泄漏、数据可能未刷入磁盘

7. MyTest —— 耗时对比评测(接口多态 + System.currentTimeMillis)

实现 —— 用缓冲流实现复制大文件并统计耗时

// 来源:course/MyTest.java
public static void main(String[] args) throws IOException {
    String src = "D:\\CentOS7\\SpringCloudAlibaba-CentOS7.zip";   // 源文件(大文件)
    String desc = "E:\\SpringCloudAlibaba-CentOS7.zip";            // 目标文件
    long start = System.currentTimeMillis();
    String message = new BufferedCopy().copy(src, desc);           // 接口实现的多态调用
    long end = System.currentTimeMillis();
    System.out.println(message + "耗时" + (end - start) + "毫秒");
}

要点:

  • 多态体现:new BufferedCopy() 直接调用 copy() 方法;因为 Copy01 与 BufferedCopy 都实现 CopyFile 接口,只要把 new 的对象换成 new Copy01() 即可对比两种实现的复制耗时——接口让"换实现"零成本
  • 耗时统计:System.currentTimeMillis() 复制前后各取一次毫秒数相减,即可得到复制耗时(毫秒)
  • 对比思路:分别用 Copy01(普通字节流)与 BufferedCopy(缓冲流)复制同一个大文件(如 SpringCloudAlibaba-CentOS7.zip),对比哪个更快——缓冲流通常显著占优

8. 字符流入门:字符集编码表 + Reader/Writer + FileReader/FileWriter(Demo02)

概念 —— 为什么需要字符流

由于字节流操作字符(特别是中文)不方便,所以 Java 提供了字符流。字符流的本质公式:字符流 = 字节流 + 字符集编码表。

// 来源:course/Demo02.java(注释部分)
/*
    由于字节流操作字符(特别是中文)不方便,所以Java提供了字符流
    字符流 = 字节流 + 字符集编码表
*/

字符集编码表(Charset)

字符集编码表是一个系统支持的所有字符的集合,包括各个国家的文字、符号、图像符号、数字等等。计算机要识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。

常见的字符编码:ASCII、GB-2312、GBK、Unicode(UTF-8)。

// 来源:course/Demo02.java(注释部分)
/*
    什么是字符集编码表?
    是一个系统支持的所有的字符的集合 ,包括各个国家的文字、符号、图像符号、数字等等
    计算机要像识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。
    常见的字符编码:ASCII、GB-2312、GBK、Unicode(UTF-8)
*/

String 类提供的编码 / 解码方法

// 来源:course/Demo02.java(注释部分)
// 编码(Encoding):字符串 → 字节数组,String 类提供
public byte[] getBytes()                        // 使用平台(操作系统)默认字符集转换
public byte[] getBytes(Charset charset)         // 使用指定的字符集转换
public byte[] getBytes(String charsetName)      // 使用指定的字符集名称转换

// 解码(Decoding):字节数组 → 字符串,String 类构造方法
public String(byte[] bytes)                     // 使用平台(操作系统)默认字符集解码
public String(byte[] bytes, Charset charset)    // 使用指定的字符集解码
public String(byte[] bytes, String charsetName) // 使用指定的字符集名称解码

要点:编码是字符串 → 字节,解码是字节 → 字符串;同一段字节用不同编码表解码会得到不同结果(乱码问题的根源)。

字符流体系 —— Reader / Writer 两大抽象父类

与字节流(InputStream / OutputStream)对称,字符流也分输入、输出两大体系,子类名同样以父类名作后缀:

字符流:
    |- 输入字符流:Reader 是输入字符流的抽象类
        |- 文件输入字符流:FileReader
    |- 输出字符流:Writer 是输出字符流的抽象类
        |- 文件输出字符流:FileWriter

FileReader / FileWriter 的构造方法

// 来源:course/Demo02.java(注释部分)
// FileReader 三种构造方法
public FileReader(String fileName)                  // 根据文件名创建,使用系统默认字符集
public FileReader(File file)                        // 根据 File 对象创建,使用系统默认字符集
public FileReader(String fileName, Charset charset) // 根据文件名创建,使用指定字符集

// FileWriter 四种构造方法(含 append 追加)
public FileWriter(String fileName)                  // 根据文件名创建,使用系统默认字符集
public FileWriter(File file)                        // 根据 File 对象创建,使用系统默认字符集
public FileWriter(File file, boolean append)        // 根据 File 对象创建,append=true 可追加
public FileWriter(String fileName, Charset charset) // 根据文件名创建,使用指定字符集

字符流读写方法(与字节流对照)

// 来源:course/Demo02.java(注释部分)
// 写方法:写入一个字符 / 字符数组 / 字符串
write(int c)                       // 写一个字符
write(char[] c)                    // 写一个字符数组的内容
write(char[] c, int off, int len)  // 写字符数组的内容,从 off 开始,长度为 len
write(String str)                  // 写一个字符串
write(String str, int off, int len)// 写字符串的一部分,从 off 开始,长度为 len
flush()                            // 刷新流,把缓冲区的内容写入到文件中
close()                            // 关闭流

// 读方法
read()                             // 读取一个字符
read(char[] c, int off, int len)   // 读取一部分字符数组

要点:

  • 字符流自带缓冲区,写操作后需要 flush() 刷新,强制把缓冲区内容写入文件
  • 字符流可以直接 write(String str) 写字符串,不需要先 getBytes() 转字节——这是它操作文本(尤其中文)比字节流方便的根本原因
  • FileReader / FileWriter 构造时可指定字符集(Charset),解决不同编码文件的乱码问题

FileWriterTest —— 写演示(六种写方法实战)

// 来源:course/FileWriterTest.java
FileWriter fw = new FileWriter("D:/abc.txt");
fw.write(97);                              // write(int c) 写一个字符(ASCII 97 = 'a')
char[] chars = {'a','b','c','d','e'};
fw.write(chars);                           // write(char[] c) 写一个字符数组
fw.write(chars, 3, 2);                     // write(char[] c, int off, int len) 从 off 开始写 len 个 → 'd' 'e'
fw.flush();                                // flush() 刷新流,把缓冲区内容写入文件
fw.write("Hello world");                   // write(String str) 直接写字符串(无需 getBytes)
fw.write("qwertyuiopasdfghjklxcvbnm", 3, 6);// write(String str, int off, int len) 写字符串子串 → "rtyuio"
fw.close();                                // close() 关闭流

FileReaderTest —— 读演示(两种读法)

// 来源:course/FileReaderTest.java
FileReader fr = new FileReader("D:/abc.txt");

// 读法一:read() 逐字符读取(一次一个字符,返回 -1 表示读完)
int ch;
while ((ch = fr.read()) != -1) {
    System.out.print((char) ch);
}

// 读法二:read(char[] c, int off, int len) 读入字符数组(推荐,批量读取)
char[] chars = new char[1024];
int count;
while ((count = fr.read(chars)) != -1) {
    System.out.print(new String(chars, 0, count));   // 字符数组 → 字符串
}
fr.close();                                        // 释放资源

要点:

  • FileWriter 直接 write(String):字符流写字符串无需 getBytes() 转换,write(String, off, len) 还可写子串
  • FileReader 与 FileInputStream 结构一致:read() 返回 -1 表示读到末尾(EOF);read(char[]) 返回本次有效字符数
  • new String(chars, 0, count):把本次读到的有效字符数组转成字符串,避免把缓冲区残留数据也转进去(与字节流 new String(bytes, 0, length, UTF-8) 同理)
  • flush() 与 close():flush() 之后还可以继续 write;close() 之前会自动刷新一次缓冲区

9. 字符缓冲流:BufferedReader / BufferedWriter(Demo03)

概念 —— 给字符流再加缓冲

字符缓冲流是字符流(Reader/Writer)的缓冲版本:BufferedWriter 把文本写入字符输出流并缓冲字符,BufferedReader 从字符输入流读取文本并缓冲字符,提供高效读写。可以指定缓冲区大小,也可以接受默认大小(默认值足够大,可满足大多数用途)。

// 来源:course/Demo03.java(注释部分)
/*
    BufferedWriter  把文本写入字符输出流,缓冲字符。提供高效写入。可以指定缓冲区的大小,或者可以接受默认大小。默认值足够大,可以用在大多数用途中。
    BufferedReader  从字符输入流中读取文本,缓冲字符。提供高效读取。可以指定缓冲区的大小,或者可以接受默认大小。默认值足够大,可以用在大多数用途中。

    public BufferedReader(Reader in)              创建缓冲字符输入流对象
    public BufferedReader(Reader in, int sz)      创建缓冲字符输入流对象,指定缓冲区的大小
    public BufferedWriter(Writer out)             创建缓冲字符输出流对象
    public BufferedWriter(Writer out, int sz)     创建缓冲字符输出流对象,指定缓冲区的大小
*/

字符缓冲流特有的方法

流 特有方法 说明
BufferedWriter newLine() 写一行行分隔符,行分隔符字符串根据系统属性定义(跨平台)
BufferedReader readLine() 读取一行字符串(不含终止字符);如果已到流的末尾,返回 null
// 来源:course/Demo03.java(注释部分)
/*
    字符缓冲流特有的方法:
    BufferedWriter:
    public void newLine() 写一行行分隔符,行分隔符字符串根据系统属性进行定义
    BufferedReader:
    public String readLine() 读取一行字符串,包含行的内容的字符串,不包括任何终止字符,
    如果已经到了流的末尾,则为null
*/

main 演示 —— 包装 FileWriter/FileReader + newLine() 写行 + readLine() 读行

// 来源:course/Demo03.java
BufferedWriter bw = new BufferedWriter(new FileWriter("D:/abc.txt"));
BufferedReader br = new BufferedReader(new FileReader("D:/abc.txt"));

// 写:write 各种写法 + newLine() 换行(跨平台行分隔符)
bw.write(97);
bw.write("hello world\r\n");
bw.write("白日依山尽");
bw.newLine();
bw.write("黄河入海流");
bw.newLine();
bw.write("哈哈哈哈哈");
bw.newLine();
bw.write("自挂东南枝");
bw.newLine();

bw.flush(); // 写完还没有关闭流,缓冲区还没有满,这时候不会写入到文件中的,所以需要 flush 强制刷新到文件中。

// 读:readLine() 逐行读取,-1 的字符流版本是 null
String line;    // 行
while ((line = br.readLine()) != null) {
    System.out.println(line);
}

bw.close();
br.close();

要点:

  • readLine() 循环读行的经典写法:while ((line = br.readLine()) != null) —— 读取一行,到流末尾返回 null(对应字节流的 -1)
  • newLine() 跨平台换行:不用手写 "\r\n",newLine() 会根据操作系统属性自动写对应的行分隔符(Windows \r\n / Linux \n)
  • flush() 的作用再次体现:写完未关流且缓冲区未满时数据不会自动落盘,必须 flush() 强制刷新
  • 与字节缓冲流(BufferedInputStream/BufferedOutputStream)对照:字符缓冲流 = 字符流 + 缓冲区,构造参数是 Reader/Writer 类型(同样可链式包装 new BufferedReader(new FileReader(...)))

10. IO 流体系小结:基础流 vs 处理流-缓冲流(Demo04)

概念 —— 今日 IO 流知识全景

Demo04 是本日 IO 流知识的小结,把今天学的所有流按"基础流 / 处理流(扩展流)"和"字节 / 字符"两个维度归类,形成完整体系:

// 来源:course/Demo04.java(注释部分)
/*
    基础流两种:
        字节流:	处理的是可以任意类型的文件
            输出字节流	OutputStream
                文件输出字节流:FileOutputStream
            输入字节流	InputStream
                文件输入字节流:FileInputStream
        字符流:	处理的是纯文本文件
            输入字符流	Reader
                文件输入字符流:FileReader
            输出字符流	Writer
                文件输出字符流:FileWriter
    处理流(扩展流)-缓冲流两种:
        缓冲字节流:	处理的是字节输入流和字节输出流。
            缓冲字节输入流:BufferedInputStream
            缓冲字节输出流:BufferedOutputStream
        缓冲字符流:	处理的是字符输入流和字符输出流。
            缓冲字符输入流:BufferedReader
            缓冲字符输出流:BufferedWriter
*/

IO 流体系总览图

                                    ┌─ 字节流(处理任意类型文件)
                                    │    ├─ 输出字节流 OutputStream
                                    │    │    └─ FileOutputStream(文件字节输出流)
                                    │    └─ 输入字节流 InputStream
                                    │         └─ FileInputStream(文件字节输入流)
IO 流 ── 基础流 ────────────────────┤
                                    │    ├─ 输入字符流 Reader
                                    │    │    └─ FileReader(文件字符输入流)
                                    └─ 字符流(处理纯文本文件)
                                         └─ 输出字符流 Writer
                                              └─ FileWriter(文件字符输出流)

         ── 处理流(扩展流)─ 缓冲流
                                    ├─ 缓冲字节流(处理字节输入/输出流)
                                    │    ├─ BufferedInputStream(缓冲字节输入流)
                                    │    └─ BufferedOutputStream(缓冲字节输出流)
                                    └─ 缓冲字符流(处理字符输入/输出流)
                                         ├─ BufferedReader(缓冲字符输入流)
                                         └─ BufferedWriter(缓冲字符输出流)

要点:

  • 两大分类维度:按数据单位分字节流 / 字符流(字节流处理任意类型文件、字符流处理纯文本文件);按是否直接操作底层分基础流 / 处理流(扩展流)
  • 命名规律:文件流子类名以父类名作后缀(FileOutputStream / FileInputStream / FileReader / FileWriter);缓冲流以 Buffered 前缀 + 父类名(BufferedInputStream / BufferedOutputStream / BufferedReader / BufferedWriter)
  • 处理流(缓冲流)"处理"的是同类型的流:缓冲字节流处理字节输入/输出流,缓冲字符流处理字符输入/输出流——构造参数对应(BufferedInputStream(InputStream) 配 FileInputStream,BufferedReader(Reader) 配 FileReader)
  • 选型口诀:不确定类型用字节流;纯文本用字符流;要效率再加对应的缓冲流

11. 随堂练习参考解答:BufferedWriteReadTest / ScoreSort —— 缓冲流写读统计 + 字符缓冲流读取排序写回

练习题目回顾

用 BufferedOutputStream 把 100 万随机大写字母写入 D:/letters.txt(每 500 个换行并 flush),再用 BufferedInputStream 逐字节读回,统计每个大写字母出现次数,并找出出现最多的字母。

写端 —— 100 万随机大写字母 + 每 500 个换行 + flush

// 来源:exericse/BufferedWriteReadTest.java(写端)
String path = "D:/letters.txt";
Random ran = new Random();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(path));  // 链式包装
for (int i = 0; i < 1000000; i++) {
    int c = ran.nextInt(26) + 65;      // 随机大写字母 A~Z
    bos.write(c);
    if (i % 500 == 0) {
        bos.write("\r\n".getBytes());  // 每 500 个写换行
        bos.flush();                   // 刷新到磁盘
    }
}
bos.close();

读端 —— 字母频次统计 int[26] + 找最多

// 来源:exericse/BufferedWriteReadTest.java(读端)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
int[] count = new int[26];             // 26 个计数器,下标 0~25 对应 A~Z
int b;
while ((b = bis.read()) != -1) {
    if (b >= 'A' && b <= 'Z')
        count[b - 'A']++;              // 核心技巧:用 ASCII 偏移量定位数组下标
}
int max = count[0];
for (int i = 0; i < count.length; i++) {
    System.out.println(count[i]);
    if (max < count[i]) max = count[i];
}
System.out.println("最多的是" + max + "次");
bis.close();

要点:

  • 字母频次统计的核心技巧:count[b - 'A']++ —— 把字符的 ASCII 码减去 'A' 得到 0~25 的下标,天然把"字母"映射到"数组下标",一次遍历完成统计(类似哈希思想)
  • 读回时用 if (b >= 'A' && b <= 'Z') 过滤掉换行符(\r\n),只统计大写字母
  • 缓冲流构造可以链式包装:new BufferedOutputStream(new FileOutputStream(path)) 一行完成"创建文件流 + 缓冲流包装"

练习六参考解答 —— ScoreSort / Student:字符缓冲流读取 + 排序 + 写回

实体类 Student(姓名 + 成绩)与主程序 ScoreSort 是本日练习六"用字符缓冲流读取文件数据、排序后写回"的参考解答,完整串起字符缓冲流 + 集合 + 排序 + 写文件的五步闭环:

// 来源:exericse/Student.java(实体类)
public class Student {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    public String getName() { return name; }
    public int getScore() { return score; }
    @Override
    public String toString() {
        return name + "-" + score;    // "姓名-成绩"
    }
}
// 来源:exericse/ScoreSort.java
BufferedReader br = new BufferedReader(new FileReader("D:/scores.txt"));  // 1、读取
ArrayList<Student> list = new ArrayList<>();
String line;
while ((line = br.readLine()) != null) {          // 逐行读取
    String[] parts = line.split("-");             // 2、处理:拆分姓名-成绩
    String name = parts[0];
    int score = Integer.parseInt(parts[1]);       //    字符串 → 整数
    list.add(new Student(name, score));           // 3、封装对象存入集合
}
Collections.sort(list, ((o1, o2) -> o2.getScore() - o1.getScore()));  // 4、排序:Lambda 按成绩降序

BufferedWriter bw = new BufferedWriter(new FileWriter("D:/sorted.txt")); // 5、写回
for (int i = 0; i < list.size(); i++) {
    bw.write("第" + (i + 1) + "名:" + list.get(i).toString());
    bw.newLine();                                 // 跨平台换行
}
bw.flush();
bw.close();
br.close();
System.out.println("排序完成!结果已写入 D:/sorted.txt");

要点:

  • Collections.sort(list, (o1, o2) -> o2.getScore() - o1.getScore()):Comparator 函数式接口用 Lambda 写法替代匿名内部类(对应前面学过的 Lambda + 函数式编程),o2 - o1 即降序(交换参数)
  • line.split("-") + Integer.parseInt():把"姓名-成绩"一行拆成两部分,成绩字符串转整数后才能按数值排序(否则按字典序,"95" 会排在 "9" 后面)
  • toString() 重写:bw.write("第" + (i + 1) + "名:" + list.get(i)) 直接拼接对象会自动调用重写的 toString() 输出"姓名-成绩"

12. 知识点全景总结

本课知识点围绕"更快更规范地复制文件"展开,一张表看清"知识 → 代码位置":

知识点 具体体现 源码位置
接口定义(契约) interface CopyFile:String copy(String src, String desc) throws IOException,抽象文件复制规范 course/CopyFile
面向接口编程 / 多态 Copy01 / BufferedCopy 两个类实现同一接口;MyTest 中可自由替换实现对比耗时 course/Copy01、BufferedCopy、MyTest
字节缓冲流 BufferedOutputStream new BufferedOutputStream(fos) 包装文件输出流,先写内部缓冲区再落盘 course/BufferedCopy、BufferedStreamTest
字节缓冲流 BufferedInputStream new BufferedInputStream(fis) 包装文件输入流,一次读多个字节填充缓冲区 course/BufferedCopy、BufferedStreamTest
缓冲流构造方法 public BufferedOutputStream(OutputStream out) / public BufferedInputStream(InputStream out),参数是底层字节流 course/Demo01 注释
内部缓冲区 缓冲流默认 8KB(8192)缓冲区,减少磁盘 I/O 次数提升效率 course/BufferedCopy、BufferedStreamTest
基础流 vs 扩展流 FileInputStream/FileOutputStream 是基础流直接操作文件;BufferedInputStream/BufferedOutputStream 是扩展流操作基础流 course/Demo01 注释
flush() 刷新 缓冲区未满时手动 flush() 强制写入磁盘 course/BufferedStreamTest
缓冲区循环复制 while ((count = bis.read(bytes)) != -1) { bos.write(bytes, 0, count); } course/BufferedCopy、Copy01
try-catch-finally 关闭资源 流变量 null 声明 + finally 中 close + IOException 包装 RuntimeException course/Copy01
throws 声明 vs 内部捕获 BufferedCopy 向上 throws IOException;Copy01 内部 try-catch 返回结果提示 course/BufferedCopy、Copy01
关闭顺序 先关缓冲流再关底层流(close 缓冲流会自动关闭底层流) course/BufferedCopy、BufferedStreamTest
Scanner 交互写入 sc.nextLine() 读取输入 → getBytes() → fos.write() 写入文件 course/Demo01
耗时统计 System.currentTimeMillis() 相减统计复制耗时,对比两种实现效率 course/MyTest
字符流概念 字符流 = 字节流 + 字符集编码表;字节流操作中文不方便,故提供字符流 course/Demo02
字符集编码表 ASCII / GB-2312 / GBK / Unicode(UTF-8);String 编码 getBytes() / 解码 new String(byte[]) 三种重载 course/Demo02
字符流体系 Reader(输入字符流抽象类)→ FileReader;Writer(输出字符流抽象类)→ FileWriter;子类名以父类名作后缀 course/Demo02
字符流构造与读写 FileReader/FileWriter 构造(可指定字符集 / append 追加);write(int/char[]/String)/read()/flush()/close();字符流自带缓冲区 course/Demo02
FileWriter 写演示 write(int)/write(char[])/write(char[],off,len)/write(String)/write(String,off,len) 六种写方法 + flush() + close();写字符串无需 getBytes course/FileWriterTest
字符缓冲流 BufferedWriter new BufferedWriter(Writer out) 包装字符输出流 + 缓冲;特有方法 newLine() 按系统属性写跨平台行分隔符 course/Demo03
字符缓冲流 BufferedReader new BufferedReader(Reader in) 包装字符输入流 + 缓冲;特有方法 readLine() 读取一行(不含终止符),到末尾返回 null course/Demo03
readLine() 循环读行 while ((line = br.readLine()) != null) { ... } —— 字符流的逐行读取(对应字节流 read 返回 -1) course/Demo03
IO 流体系小结 基础流(字节流处理任意文件 / 字符流处理纯文本)vs 处理流-缓冲流(缓冲字节流 / 缓冲字符流)完整分类 course/Demo04
Lambda 排序 Collections.sort(list, (o1, o2) -> o2.getScore() - o1.getScore()) Comparator 函数式接口 Lambda 写法按成绩降序 exericse/ScoreSort
读取排序写回闭环 BufferedReader 读取 → split 拆分 → Integer.parseInt 转换 → ArrayList 存对象 → sort 排序 → BufferedWriter newLine 写回 exericse/ScoreSort、Student
FileReader 读演示 read() 逐字符读取(返回 -1 表示 EOF)/ read(char[]) 读入字符数组;new String(chars,0,count) 转字符串;fr.close() 释放资源 course/FileReaderTest
字母频次统计 int[26] + count[b-'A']++ 一次遍历统计;if (b >= 'A' && b <= 'Z') 过滤换行符;找出最大值 exericse/BufferedWriteReadTest

13. 随堂练习要点

  • 用 Copy01 和 BufferedCopy 分别复制同一个大文件(视频 / zip / exe),用 System.currentTimeMillis() 统计耗时,对比普通字节流 vs 缓冲流的复制效率;再尝试改变 byte[] 数组大小(1024 / 4096 / 8192 / 16384)观察耗时变化
  • 用 BufferedOutputStream 写入 100 万字符并每 500 个字符 flush() 一次;去掉 flush 对比文件内容差异,理解 flush() 的作用(缓冲区未满时数据不会自动落盘)
  • 把 BufferedStreamTest 写端注释取消,用 BufferedInputStream 读回文件内容,验证写入读取闭环
  • 参考 Copy01 的 try-catch-finally 写法,把 BufferedCopy 也改造成"内部捕获异常返回结果提示"的版本,对比两种异常处理风格
  • 思考:Copy01 只用 byte[8192] 数组、BufferedCopy 在缓冲流之上再加 byte[8192] 数组,两层缓冲是否必要?去掉外层数组(缓冲流逐字节 write/read)耗时会不会有明显变化?
  • 用接口变量接收实现:CopyFile cf = new BufferedCopy(); cf.copy(src, desc);,再换成 new Copy01(),体会面向接口编程的替换性
  • 完成 exericse/BufferedWriteReadTest 练习(参考第 9 节解答):100 万随机大写字母写入 + 逐字节读回 + int[26] 字母频次统计 + 找出现最多的字母
  • 字符流练习:用 FileWriter 写入一段中文文本(体会无需 getBytes() 直接 write(String)),再用 FileReader 读回显示,对比字节流操作中文的差异
  • 字符集练习:用 FileWriter(..., Charset.forName("GBK")) 写一个文件,再用默认字符集的 FileReader 读取,观察乱码现象;改用 FileReader(..., Charset.forName("GBK")) 指定同一字符集读,验证乱码消失
  • 在 BufferedWriteReadTest 基础上拓展:把字母频次按出现次数从高到低排序输出字母排行(int[] + 冒泡/选择排序或 HashMap + 比较器)
  • 字符缓冲流练习:把 FileWriterTest / FileReaderTest 改写为 BufferedWriter / BufferedReader 版本——用 newLine() 写多行中文(体会跨平台换行),用 readLine() 逐行读回(体会 while ((line = br.readLine()) != null) 写法)
  • 字符缓冲流对比练习:分别用 FileReader(read(char[]))与 BufferedReader(readLine())读取同一个文件,对比两种写法的代码简洁度;思考 readLine() 为什么能"一次读一行"
  • 综合练习(参考第 11 节 ScoreSort 解答):完成"字符缓冲流读取文件 → 排序 → 写回"——D:/scores.txt 中"姓名-成绩"乱序,用 BufferedReader 逐行读 + split("-") 拆分 + ArrayList 存 Student 对象 + Collections.sort(Lambda)按成绩降序 + BufferedWriter 写回 D:/sorted.txt 带名次结果
  • 体系梳理练习:仿照 Demo04 小结,自己画出今日 IO 流的完整体系图(基础流:字节/字符 + 输入/输出;处理流:缓冲字节/缓冲字符),并说出每个流"处理什么类型、怎么配对包装"

14. 拓展阅读

  • JDK 文档:java.io.BufferedInputStream、java.io.BufferedOutputStream、java.io.Reader、java.io.Writer、java.io.FileReader、java.io.FileWriter、java.io.BufferedReader、java.io.BufferedWriter
  • 缓冲流的本质:缓冲流是"装饰器模式(Decorator)"在 Java I/O 中的经典应用——通过包装已有的流对象,在不改变流接口的前提下动态增强功能(增加缓冲能力)
  • try-with-resources(JDK7+):try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src))) { ... } 可在 try 结束时自动关闭所有流,无需手写 finally close,是更现代的资源管理方式
  • 关闭流的传递性:调用 bis.close() 会级联关闭内部包装的 fis,因此生产代码通常只需关闭最外层流;但为了保证幂等与可读性,逐层显式关闭也常见
  • 字符缓冲流:字节缓冲流之后,还有配套的字符缓冲流 BufferedReader / BufferedWriter,其中 BufferedReader.readLine() 一次读一行,非常适合纯文本文件;BufferedWriter.newLine() 自动写系统对应的行分隔符
  • flush() 与 close() 的关系:close() 之前会自动调用一次 flush() 把残留数据写入磁盘,所以只写不 flush 只要 close 也能完整落盘;flush() 用于"不关闭流但需要及时看到数据"的场景
  • 字符流 vs 字节流选型:纯文本(.txt)用字符流(读中文方便、可直接 write(String));图片 / 视频等二进制文件用字节流;字符流底层仍基于字节,由编码表负责"字节 ↔ 字符"的转换
  • 乱码与字符集:文件保存编码(如 GBK)与读取时指定编码(如 UTF-8)不一致就会产生乱码;FileReader / FileWriter 构造时用 Charset 显式指定编码可避免
  • IO 流学习路线:File(路径管理)→ 文件字节流(FileInputStream/FileOutputStream)→ 字节缓冲流(BufferedInputStream/BufferedOutputStream)→ 字符流(Reader/Writer + FileReader/FileWriter)→ 字符缓冲流(BufferedReader/BufferedWriter)→ 转换流 / 对象流 / 打印流等,缓冲流是"性能优化"的重要一环