# 20260807 课堂笔记 — IO 缓冲流与字符流 - **日期**:2026-08-07 - **项目**:`c260807` - **包路径**:`course` - **作者**:WanJL --- ## 目录 1. [接口化设计:CopyFile 接口 —— 统一文件复制的规范](#1-接口化设计copyfile-接口--统一文件复制的规范) 2. [字节缓冲流概念:BufferedOutputStream / BufferedInputStream(基础流 vs 扩展流)](#2-字节缓冲流概念bufferedoutputstream--bufferedinputstream基础流-vs-扩展流) 3. [Copy01 —— 普通字节流复制实现(byte[] 数组缓冲区 + try-catch-finally 关闭资源)](#3-copy01--普通字节流复制实现byte-数组缓冲区--try-catch-finally-关闭资源) 4. [BufferedCopy —— 缓冲流复制实现(缓冲流包装底层字节流)](#4-bufferedcopy--缓冲流复制实现缓冲流包装底层字节流) 5. [BufferedStreamTest —— 缓冲流写入 100 万字符(flush 刷新)与读取演示](#5-bufferedstreamtest--缓冲流写入-100-万字符flush-刷新与读取演示) 6. [Demo01 —— Scanner 交互式写入(输入一行写一行)](#6-demo01--scanner-交互式写入输入一行写一行) 7. [MyTest —— 耗时对比评测(接口多态 + System.currentTimeMillis)](#7-mytest--耗时对比评测接口多态--systemcurrenttimemillis) 8. [字符流入门:字符集编码表 + Reader/Writer + FileReader/FileWriter(Demo02)](#8-字符流入门字符集编码表--readerwriter--filereaderfilewriterdemo02) 9. [字符缓冲流:BufferedReader / BufferedWriter(Demo03)](#9-字符缓冲流bufferedreader--bufferedwriterdemo03) 10. [IO 流体系小结:基础流 vs 处理流-缓冲流(Demo04)](#10-io-流体系小结基础流-vs-处理流-缓冲流demo04) 11. [随堂练习参考解答:BufferedWriteReadTest / ScoreSort —— 缓冲流写读统计 + 字符缓冲流读取排序写回](#11-随堂练习参考解答bufferedwritereadtest--scoresort--缓冲流写读统计--字符缓冲流读取排序写回) 12. [知识点全景总结](#12-知识点全景总结) 13. [随堂练习要点](#13-随堂练习要点) 14. [拓展阅读](#14-拓展阅读) --- ## 1. 接口化设计:CopyFile 接口 —— 统一文件复制的规范 ### 概念 —— 接口作为"文件复制"的契约 在文件复制的场景中,先定义一个 **`CopyFile` 接口**(复制器接口),抽象出"复制文件"这个方法规范:传入**原路径 src** 与**新路径 desc**,返回一个**复制结果提示字符串**,并声明抛 `IOException`(因为实现类创建流、读写文件都可能抛编译期异常)。 ```java // 来源: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 扩展流) ### 概念 —— 什么是字节缓冲流 ```java // 来源: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 代码同步更新) ```java // 来源: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** 保证资源一定关闭: ```java // 来源: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]` 数组作为"二次缓冲": ```java // 来源: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 ```java // 来源: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(); ``` ### 读端 —— 缓冲流逐字节读取显示 ```java // 来源: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 交互式写入(输入一行写一行) ### 实现 —— 控制台输入即写入文件 ```java // 来源: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) ### 实现 —— 用缓冲流实现复制大文件并统计耗时 ```java // 来源: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 提供了**字符流**。字符流的本质公式:**字符流 = 字节流 + 字符集编码表**。 ```java // 来源:course/Demo02.java(注释部分) /* 由于字节流操作字符(特别是中文)不方便,所以Java提供了字符流 字符流 = 字节流 + 字符集编码表 */ ``` ### 字符集编码表(Charset) **字符集编码表**是一个系统支持的**所有字符的集合**,包括各个国家的文字、符号、图像符号、数字等等。计算机要识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。 常见的字符编码:**ASCII、GB-2312、GBK、Unicode(UTF-8)**。 ```java // 来源:course/Demo02.java(注释部分) /* 什么是字符集编码表? 是一个系统支持的所有的字符的集合 ,包括各个国家的文字、符号、图像符号、数字等等 计算机要像识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。 常见的字符编码:ASCII、GB-2312、GBK、Unicode(UTF-8) */ ``` ### String 类提供的编码 / 解码方法 ```java // 来源: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 的构造方法 ```java // 来源: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) // 根据文件名创建,使用指定字符集 ``` ### 字符流读写方法(与字节流对照) ```java // 来源: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 —— 写演示(六种写方法实战) ```java // 来源: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 —— 读演示(两种读法) ```java // 来源: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` 从字符输入流读取文本并缓冲字符,提供**高效读写**。可以指定缓冲区大小,也可以接受默认大小(默认值足够大,可满足大多数用途)。 ```java // 来源: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** | ```java // 来源:course/Demo03.java(注释部分) /* 字符缓冲流特有的方法: BufferedWriter: public void newLine() 写一行行分隔符,行分隔符字符串根据系统属性进行定义 BufferedReader: public String readLine() 读取一行字符串,包含行的内容的字符串,不包括任何终止字符, 如果已经到了流的末尾,则为null */ ``` ### main 演示 —— 包装 FileWriter/FileReader + newLine() 写行 + readLine() 读行 ```java // 来源: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 流知识的**小结**,把今天学的所有流按"基础流 / 处理流(扩展流)"和"字节 / 字符"两个维度归类,形成完整体系: ```java // 来源: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 ```java // 来源: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] + 找最多 ```java // 来源: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` 是本日练习六"用字符缓冲流读取文件数据、排序后写回"的参考解答,完整串起**字符缓冲流 + 集合 + 排序 + 写文件**的五步闭环: ```java // 来源: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; // "姓名-成绩" } } ``` ```java // 来源:exericse/ScoreSort.java BufferedReader br = new BufferedReader(new FileReader("D:/scores.txt")); // 1、读取 ArrayList 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`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/BufferedInputStream.html)、[`java.io.BufferedOutputStream`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/BufferedOutputStream.html)、[`java.io.Reader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Reader.html)、[`java.io.Writer`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Writer.html)、[`java.io.FileReader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/FileReader.html)、[`java.io.FileWriter`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/FileWriter.html)、[`java.io.BufferedReader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/BufferedReader.html)、[`java.io.BufferedWriter`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/BufferedWriter.html) - **缓冲流的本质**:缓冲流是"装饰器模式(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)→ 转换流 / 对象流 / 打印流等,缓冲流是"性能优化"的重要一环