# 2026-08-07 IO 字节缓冲流 + 字符流练习(课堂练习用) > **说明**:练习今日新学知识点——**IO 字节缓冲流**(`BufferedOutputStream` / `BufferedInputStream`:内部缓冲区、`flush()` 刷新)与**接口化文件复制**(`CopyFile` 接口 + `Copy01` 普通字节流实现 / `BufferedCopy` 缓冲流实现):try-catch-finally 关闭资源、`System.currentTimeMillis()` 耗时对比;以及**字符流**(`Reader` / `Writer` + `FileReader` / `FileWriter`:直接 `write(String)` 写中文无需 `getBytes()`、字符流 = 字节流 + 字符集编码表、`Charset` 字符集与乱码原理)与**字符缓冲流**(`BufferedReader` / `BufferedWriter`:`readLine()` 逐行读取 / `newLine()` 换行、读 → 处理 → 集合 → 排序 → 写回文件)。 --- ## 练习一:文件复制器效率对比(普通字节流 vs 缓冲流) **难度**:⭐⭐⭐ **知识点**:`CopyFile` 接口、`FileInputStream` / `FileOutputStream`、`BufferedInputStream` / `BufferedOutputStream`、`byte[]` 缓冲区、try-catch-finally、`System.currentTimeMillis()` **场景描述**:课堂用 `CopyFile` 接口统一了"文件复制"的规范,并实现了两个版本:`Copy01`(普通字节流 + `byte[8192]` 数组)和 `BufferedCopy`(缓冲流包装底层流)。现在把两个实现整合进一个**对比程序**,复制同一个大文件,用耗时说话——验证"缓冲流更快"。 ### 代码框架 ```java import java.io.*; // ① 复制器接口:统一复制规范 public interface CopyFile { String copy(String src, String desc) throws IOException; } // ② 普通字节流实现:FileInputStream + FileOutputStream + byte[8192] 数组 + try-catch-finally class Copy01 implements CopyFile { @Override public String copy(String src, String desc) { FileOutputStream fos = null; FileInputStream fis = null; try { // TODO 1: 创建输出流 fos 和输入流 fis // TODO 2: byte[8192] 缓冲区 + while 循环「读一批 → 写一批」 } catch (IOException e) { return "文件复制失败"; } finally { // TODO 3: finally 中关闭 fos / fis // 提示:close() 也可能抛 IOException → 包装成 RuntimeException } return "文件复制成功"; } } // ③ 缓冲流实现:BufferedOutputStream / BufferedInputStream 包装底层流 class BufferedCopy implements CopyFile { @Override public String copy(String src, String desc) throws IOException { // TODO 4: 先创建文件流,再用缓冲流包装它们 // TODO 5: byte[8192] 数组 + while ((count = bis.read(bytes)) != -1) { bos.write(bytes, 0, count); } // TODO 6: 关闭:先关缓冲流再关底层流 return "复制成功"; } } // ④ 测试类:对比两种实现复制同一个大文件的耗时 public class CopyCompareTest { public static void main(String[] args) throws IOException { String src = "D:/大文件.zip"; // 换成你电脑上任意大于 10MB 的文件 // TODO 7: 用 new Copy01().copy(src, ...) 复制,System.currentTimeMillis() 统计耗时 // TODO 8: 用 new BufferedCopy().copy(src, ...) 复制,统计耗时 // TODO 9: 输出两次耗时,对比哪个更快 } } ``` **选做加分**: - 把缓冲区大小改成 1024 / 4096 / 8192 / 16384 各测一次,总结"缓冲区大小 → 耗时"的变化规律 - 把 `Copy01` 改成"失败时返回提示信息"的 try-catch 风格,`BufferedCopy` 保持 throws 风格,体会两种异常处理方式 **思考题**: 1. 复制大文件时 `BufferedCopy` 为什么通常比 `Copy01` 更快?(提示:缓冲流内部缓冲区减少与磁盘的交互次数) 2. `BufferedCopy` 在缓冲流之上又用了 `byte[8192]` 数组,这层"双缓冲"是必须的吗?(提示:可以去掉外层数组试试看耗时变化) 3. 为什么 finally 中关闭流时还要再写一层 try-catch?(提示:`close()` 也声明抛 `IOException`) 4. 关闭顺序为什么是"先关缓冲流、再关底层流"?(提示:close 缓冲流会自动关闭它包装的底层流) --- ## 练习二:百万字符写入 + 读取统计(flush 体验 + 字母频次统计) **难度**:⭐⭐⭐ **知识点**:`BufferedOutputStream`、`BufferedInputStream`、`flush()`、`Random` 随机数、`getBytes()`、ASCII 码、数组统计 **场景描述**:课堂用 `BufferedOutputStream` 向文件写入了 100 万随机大写字母并演示了 `flush()`。现在把它做成一个**完整闭环**:写入 100 万随机字母 → 关闭流 → 再用 `BufferedInputStream` 读回 → 统计每个大写字母出现的次数 → 输出出现最多的字母。 ### 代码框架 ```java import java.io.*; import java.util.Random; public class BufferedWriteReadTest { public static void main(String[] args) throws IOException { String path = "D:/letters.txt"; Random ran = new Random(); // ========== 写:100 万随机大写字母,每 500 个换行 + flush ========== // TODO 1: 创建 FileOutputStream + BufferedOutputStream 包装它 // TODO 2: for 循环 100 万次,ran.nextInt(26) + 65 生成 A~Z,bos.write(c) 先写入内部缓冲区 // TODO 3: 每 500 个写一次 "\r\n".getBytes(),并调用 bos.flush() 刷新到磁盘 // TODO 4: 关闭缓冲流和文件流 // ========== 读:BufferedInputStream 逐字节读回并统计 ========== // TODO 5: 创建 FileInputStream + BufferedInputStream 包装它 // TODO 6: int[] count = new int[26]; 统计每个大写字母出现次数 // while ((b = bis.read()) != -1) { if (b >= 'A' && b <= 'Z') count[b - 'A']++; } // TODO 7: 遍历 count 输出每个字母出现次数;找出出现最多的字母 // TODO 8: 关闭流 } } ``` **输出示例**: ``` 字母出现统计: A: 38462 B: 38610 ... Q: 38911 ← 出现最多的字母 ``` **选做加分**: - 把写入范围扩展为**大小写字母 + 数字**(共 62 种),统计时用 `Character.isLetterOrDigit()` 判断 - 统计换行符 `\r\n` 出现的次数,验证是否符合预期(约 1000000 / 500 ≈ 2000 次) - 对比:写入时**去掉所有 flush()**,程序结束后立刻查看文件内容大小,观察是否完整(理解 `close()` 会自动 flush) **思考题**: 1. 不调用 `flush()` 时,数据什么时候才会真正写入磁盘?(提示:缓冲区满 8KB 或 `close()` 时自动刷) 2. 为什么 `bis.read()` 逐字节读取也很快?(提示:数据预先成批读入内部缓冲区,读取其实发生在内存) 3. `read()` 返回 -1 表示什么?`(char) b` 强转后为什么能显示字母?(提示:EOF;读到的字节值是 ASCII 码) 4. `ran.nextInt(26) + 65` 为什么能生成 A~Z?(提示:65~90 正好对应字符 'A'~'Z') --- ## 练习三:字符流中文复制器(FileReader / FileWriter) **难度**:⭐⭐⭐ **知识点**:`Reader` / `Writer` 抽象父类、`FileReader` / `FileWriter`、`char[]` 字符缓冲区、`write(String)` 直接写中文、字符流 vs 字节流 **场景描述**:课堂学了 `FileReader` / `FileWriter`:**字符流 = 字节流 + 字符集编码表**,写中文不用 `getBytes()`。现在用字符流写一个"中文复制器":`FileWriter` 写一段中文 → `FileReader` 读回并显示到控制台,实现"字符流三步走"的完整闭环。 ### 代码框架 ```java import java.io.*; public class CharStreamTest { public static void main(String[] args) throws IOException { // ========== 写:FileWriter 直接写中文(无需 getBytes) ========== // TODO 1: new FileWriter("D:/中文.txt") 创建文件字符输出流 // TODO 2: fw.write("床前明月光,疑是地上霜。"); 直接写中文字符串 // fw.write("\r\n"); // fw.write("举头望明月,低头思故乡。"); // TODO 3: fw.flush(); 刷新缓冲区(字符流自带缓冲区) // TODO 4: fw.close(); 关闭流 // ========== 读:FileReader 用 char[1024] 缓冲区读回 ========== // TODO 5: new FileReader("D:/中文.txt") 创建文件字符输入流 // TODO 6: char[] chars = new char[1024]; // while ((count = fr.read(chars)) != -1) { // System.out.print(new String(chars, 0, count)); // 字符数组 → 字符串 // } // TODO 7: fr.close(); 关闭流 } } ``` **选做加分**: - 实现 `CopyFile` 接口的第三个版本 `CharCopy`:用 `FileReader` + `FileWriter` + `char[1024]` 复制含中文的 txt 文件,对比与字节流版本(Copy01)复制中文文件的差异 - 用 `BufferedReader` / `BufferedWriter` 包装字符流后复制,体会"字符流 + 字符缓冲流"的组合,`BufferedReader.readLine()` 一次读一行 - 统计文件中汉字个数(提示:读到的 char 值 > 127 即为非 ASCII 的中文字符) **思考题**: 1. 为什么 `fw.write("床前明月光")` 不用像字节流那样先 `getBytes()`?(提示:字符流内部按字符集编码表完成"字符 → 字节"转换) 2. `fr.read(chars)` 返回的是什么?(提示:本次读到的有效字符个数;-1 表示读完) 3. `new String(chars, 0, count)` 的作用是什么?(提示:只取本次有效部分,避免把缓冲区残留脏数据也转进去) --- ## 练习四:字符集(Charset)与乱码实验(GBK vs UTF-8) **难度**:⭐⭐⭐⭐ **知识点**:`Charset` 字符集、`FileWriter(String, Charset)` / `FileReader(String, Charset)` 指定编码、String 编码 `getBytes(Charset)` / 解码 `new String(byte[], Charset)`、乱码原理 **场景描述**:课堂讲了"字符流 = 字节流 + 字符集编码表",乱码的根源是**读写字符集不一致**。现在做个实验:用 **GBK** 字符集写入中文,再用**默认字符集(UTF-8)**读取 → 观察乱码;改用 **GBK** 字符集读取 → 中文正常显示。 ### 代码框架 ```java import java.io.*; import java.nio.charset.Charset; public class CharsetTest { public static void main(String[] args) throws IOException { // ========== 写:用 GBK 字符集写入中文 ========== // TODO 1: new FileWriter("D:/编码.txt", Charset.forName("GBK")) // TODO 2: fw.write("中华人民共和国"); 直接写中文 // TODO 3: fw.close(); // ========== 读一:默认字符集(UTF-8)读 → 预期乱码 ========== // TODO 4: new FileReader("D:/编码.txt") 默认字符集读取 // TODO 5: char[1024] 读回显示,观察是否出现乱码 // ========== 读二:指定 GBK 字符集读 → 预期正常 ========== // TODO 6: new FileReader("D:/编码.txt", Charset.forName("GBK")) // TODO 7: 再次读回显示,观察中文是否恢复正常 } } ``` **输出示例**: ``` 默认字符集(UTF-8)读取:�й????????? ← 乱码 指定字符集(GBK)读取:中华人民共和国 ← 正常 ``` **选做加分**: - 用 String 的编码 / 解码方法演示乱码原理:`"中国".getBytes(Charset.forName("GBK"))` 转成字节数组,再用 `new String(bytes, Charset.forName("UTF-8"))` 解码,观察输出乱码 - 分别用 GBK / UTF-8 写入同一段中文,打印文件大小,对比**一个汉字占几个字节**(GBK 汉字 2 字节,UTF-8 汉字 3 字节) - 改用 `getBytes("GBK")` + 字节流 `FileOutputStream` 写文件,验证"字符流"与"字节流 + 手动编码"是等价的 **思考题**: 1. 为什么用 UTF-8 读 GBK 写的文件会乱码?(提示:编码表不一致,字节无法正确映射回字符) 2. 一个汉字在 GBK 和 UTF-8 中各占几个字节?(提示:GBK 2 字节,UTF-8 3 字节) 3. `FileWriter` 不指定字符集时用的是哪个字符集?(提示:平台操作系统默认字符集) --- ## 练习五:用户信息永久化存储(Scanner 键盘录入 + FileWriter 写入本地文件) **难度**:⭐⭐⭐ **知识点**:`Scanner` 键盘录入、`FileWriter` 字符流写入、`String` 拼接、`flush()` 刷新、`close()` 关闭流释放资源 **场景描述**:程序中的数据只存在于内存,程序结束就丢失了。要实现**永久化存储**,就要把数据写入本地文件(磁盘)。本例实现一个简单的**用户注册**:从键盘录入用户名和密码,保存到 `D:/user.txt` 文件中——程序下次运行,文件里的数据依然存在。 ### 案例需求 将键盘录入的用户名和密码保存到本地,实现永久化存储。 ### 实现步骤 1. 获取用户输入的用户名和密码 2. 将用户输入的用户名和密码写入到本地文件中 3. 关流,释放资源 ### 代码框架 ```java import java.io.FileWriter; import java.io.IOException; import java.util.Scanner; public class UserRegister { public static void main(String[] args) throws IOException { // ========== 1、获取用户输入的用户名和密码 ========== // TODO 1: 创建 Scanner 对象读取键盘输入 // TODO 2: 提示"请输入用户名:",sc.nextLine() 接收用户名 // TODO 3: 提示"请输入密码:",sc.nextLine() 接收密码 // ========== 2、将用户名和密码写入到本地文件中 ========== // TODO 4: new FileWriter("D:/user.txt") 创建文件字符输出流 // TODO 5: fw.write("用户名:" + username); fw.write("\r\n"); // fw.write("密码:" + password); // TODO 6: fw.flush(); 把缓冲区内容刷入文件 // ========== 3、关流,释放资源 ========== // TODO 7: fw.close(); 关闭流 System.out.println("注册成功!信息已保存到 D:/user.txt"); } } ``` **输出示例**: ``` 请输入用户名:admin 请输入密码:123456 注册成功!信息已保存到 D:/user.txt ``` (程序运行后打开 `D:/user.txt` 查看内容) ``` 用户名:admin 密码:123456 ``` **选做加分**: - 升级为"注册 + 登录"小系统:注册时把用户名密码写入文件,登录时用 `FileReader` 读取文件,校验输入的用户名密码与文件中的是否匹配 - 用 **追加模式** `new FileWriter("D:/user.txt", true)` 实现多用户注册:一次运行保存一组,再次运行追加一组,不覆盖之前的数据 - 用 `BufferedWriter` 包装后写入,体会"字符流 + 字符缓冲流"的组合写法 **思考题**: 1. 为什么写入后要 `flush()`?不写会怎样?(提示:字符流自带缓冲区,数据先到缓冲区,缓冲区满或 `close()` 时才落盘) 2. 程序关闭后重新运行,文件里的数据还在吗?为什么?(提示:数据已写入磁盘文件,实现了永久化存储) 3. 如果把 `FileWriter` 换成 `FileOutputStream`,写之前需要做什么?(提示:字符串要先 `getBytes()` 转成字节数组) 4. 默认覆盖写和 `append=true` 追加写有什么区别?(提示:默认每次运行会覆盖原文件内容) --- ## 练习六:字符缓冲流读取 + 排序 + 写回文件(学生成绩排行榜) **难度**:⭐⭐⭐⭐ **知识点**:`BufferedReader` / `BufferedWriter`、`readLine()` 逐行读取 / `newLine()` 写入、`String.split()` 拆分、`ArrayList` 集合、`Collections.sort()` + `Comparator` 排序、`Integer.parseInt()` 字符串转整数 **场景描述**:`D:/scores.txt` 中保存了 6 名学生的成绩,格式为"姓名-成绩",**顺序是乱的**。用 `BufferedReader` 逐行读取 → `split("-")` 拆分成姓名和成绩 → 封装成 `Student` 对象存入 `ArrayList` → 按成绩从高到低排序 → 用 `BufferedWriter` 把带名次的排序结果写回 `D:/sorted.txt`——把"读文件 → 处理 → 集合 → 排序 → 写文件"串成一个完整闭环。 ### 案例需求 使用字符缓冲流读取文件中的数据,排序后再次写到本地文件。 ### 实现步骤 1. 将文件中的数据读取到程序中 2. 对读取到的数据进行处理 3. 将处理后的数据添加到集合中 4. 对集合中的数据进行排序 5. 将排序后的集合中的数据写入到文件中 ### 原始数据 `D:/scores.txt`(顺序是乱的) ``` 李四-78 王五-92 张三-66 赵六-85 孙七-95 周八-88 ``` ### 代码框架 ```java import java.io.*; import java.util.*; // 学生类:姓名 + 成绩 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; } } public class ScoreSort { public static void main(String[] args) throws IOException { // ========== 1、将文件中的数据读取到程序中 ========== // TODO 1: new BufferedReader(new FileReader("D:/scores.txt")) 字符缓冲流包装 // TODO 2: while ((line = br.readLine()) != null) { ... } 逐行读取 // ========== 2、对读取到的数据进行处理 ========== // TODO 3: String[] parts = line.split("-"); 按 "-" 拆分成 [姓名, 成绩] // TODO 4: int score = Integer.parseInt(parts[1]); 成绩字符串 → 整数 // ========== 3、将处理后的数据添加到集合中 ========== // TODO 5: new Student(parts[0], score) 封装对象,list.add(stu) 存入 ArrayList // ========== 4、对集合中的数据进行排序 ========== // TODO 6: Collections.sort(list, new Comparator() { ... }); // 按成绩【降序】:o2.getScore() - o1.getScore() // ========== 5、将排序后的集合中的数据写入到文件中 ========== // TODO 7: new BufferedWriter(new FileWriter("D:/sorted.txt")) // TODO 8: for 遍历排序后的集合,bw.write("第" + (i+1) + "名:" + 学生) + bw.newLine() // TODO 9: bw.flush(); bw.close(); br.close(); System.out.println("排序完成!结果已写入 D:/sorted.txt"); } } ``` **输出示例**: ``` 排序完成!结果已写入 D:/sorted.txt ``` (程序运行后打开 `D:/sorted.txt`) ``` 第1名:孙七-95 第2名:王五-92 第3名:周八-88 第4名:赵六-85 第5名:李四-78 第6名:张三-66 ``` **选做加分**: - **二次排序**:成绩相同时按姓名**字典序升序**(Comparator 先比成绩降序,成绩相等再比姓名 `o1.getName().compareTo(o2.getName())`) - 用 **`TreeSet` 自动排序**:让 `Student implements Comparable`(compareTo 按成绩降序),`new TreeSet<>(list)` 自动排序去重 - **脏数据过滤**:读取时跳过成绩不在 0~100 范围的行(`if (score < 0 || score > 100) continue;`) - 排序结果**同时输出到控制台和文件**,验证写入内容正确 **思考题**: 1. `readLine()` 返回 null 表示什么?`while ((line = br.readLine()) != null)` 循环靠什么终止?(提示:读到文件末尾返回 null) 2. `line.split("-")` 拆分后返回什么类型?如果某一行没有 "-" 会怎样?(提示:返回 String[];不含分隔符则返回整行组成的长度 1 数组) 3. `Integer.parseInt(parts[1])` 的作用是什么?不转换直接用 `parts[1]` 排序,比较的是数字还是字符串?(提示:字符串比较按字典序,"95" 会排在 "9" 之后) 4. 为什么写回时用 `newLine()` 而不是手写 `"\r\n"`?(提示:跨平台行分隔符,Windows `\r\n` / Linux `\n`) 5. 不调用 `flush()` 或 `close()`,排序结果能立刻出现在文件中吗?(提示:字符缓冲流有缓冲区,不刷新不落盘) --- # 参考答案要点 ## 练习一:Copy01 / BufferedCopy / CopyCompareTest ```java // Copy01.copy 核心(TODO 1~3):普通字节流 + byte[8192] 数组 + try-catch-finally 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(); fis.close(); } catch (IOException e) { throw new RuntimeException(e); // close 异常包装成运行时异常 } } return "文件复制成功"; ``` ```java // BufferedCopy.copy 核心(TODO 4~6):缓冲流包装底层流 + 双缓冲 FileOutputStream fos = new FileOutputStream(desc); BufferedOutputStream bos = new BufferedOutputStream(fos); // 缓冲输出流包装文件输出流 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 "复制成功"; ``` ```java // CopyCompareTest.main 核心(TODO 7~9):两次复制 + 两次计时对比 String src = "D:/大文件.zip"; long start = System.currentTimeMillis(); new Copy01().copy(src, "D:/copy1.zip"); long mid = System.currentTimeMillis(); new BufferedCopy().copy(src, "D:/copy2.zip"); long end = System.currentTimeMillis(); System.out.println("普通字节流耗时:" + (mid - start) + " 毫秒"); System.out.println("缓冲流耗时:" + (end - mid) + " 毫秒"); ``` ## 练习二:BufferedWriteReadTest ```java // 写(TODO 1~4):100 万随机大写字母 + 每 500 个换行 + flush FileOutputStream fos = new FileOutputStream("D:/letters.txt"); BufferedOutputStream bos = new BufferedOutputStream(fos); Random ran = new Random(); for (int i = 0; i < 1000000; i++) { int c = ran.nextInt(26) + 65; // 65~90 → 'A'~'Z' bos.write(c); // 先写入内部缓冲区 if (i % 500 == 0) { bos.write("\r\n".getBytes()); // 每 500 个字母换一行 bos.flush(); // 手动把缓冲区数据刷到磁盘 } } bos.close(); fos.close(); // 读 + 统计(TODO 5~8):逐字节读回,统计每个大写字母出现次数 FileInputStream fis = new FileInputStream("D:/letters.txt"); BufferedInputStream bis = new BufferedInputStream(fis); int[] count = new int[26]; // 下标 0~25 对应 'A'~'Z' int b; while ((b = bis.read()) != -1) { if (b >= 'A' && b <= 'Z') { // 过滤换行符等非字母字符 count[b - 'A']++; } } bis.close(); fis.close(); int max = 0; char maxChar = 'A'; for (int i = 0; i < count.length; i++) { System.out.println((char) ('A' + i) + ": " + count[i]); if (count[i] > max) { max = count[i]; maxChar = (char) ('A' + i); } } System.out.println("出现最多的字母:" + maxChar + ",共 " + max + " 次"); ``` ## 练习三:CharStreamTest ```java // 写(TODO 1~4):FileWriter 直接写中文,无需 getBytes FileWriter fw = new FileWriter("D:/中文.txt"); fw.write("床前明月光,疑是地上霜。"); fw.write("\r\n"); fw.write("举头望明月,低头思故乡。"); fw.flush(); // 字符流自带缓冲区,flush 刷新到文件 fw.close(); // 读(TODO 5~7):FileReader + char[1024] 批量读取,字符数组 → 字符串 FileReader fr = new FileReader("D:/中文.txt"); char[] chars = new char[1024]; int count; while ((count = fr.read(chars)) != -1) { System.out.print(new String(chars, 0, count)); // 只取本次有效部分 } fr.close(); ``` ```java // 选做:CopyFile 接口的字符流实现(char[1024] 复制中文 txt) class CharCopy implements CopyFile { @Override public String copy(String src, String desc) throws IOException { FileReader fr = new FileReader(src); FileWriter fw = new FileWriter(desc); char[] chars = new char[1024]; int count; while ((count = fr.read(chars)) != -1) { fw.write(chars, 0, count); } fw.close(); fr.close(); return "复制成功"; } } ``` ## 练习四:CharsetTest ```java // 写(TODO 1~3):指定 GBK 字符集写入中文 FileWriter fw = new FileWriter("D:/编码.txt", Charset.forName("GBK")); fw.write("中华人民共和国"); fw.close(); // 读一(TODO 4~5):默认字符集(UTF-8)读 → 乱码 FileReader fr1 = new FileReader("D:/编码.txt"); char[] chars = new char[1024]; int count; while ((count = fr1.read(chars)) != -1) { System.out.println("默认字符集(UTF-8)读取:" + new String(chars, 0, count)); // 乱码 } fr1.close(); // 读二(TODO 6~7):指定 GBK 字符集读 → 正常 FileReader fr2 = new FileReader("D:/编码.txt", Charset.forName("GBK")); while ((count = fr2.read(chars)) != -1) { System.out.println("指定字符集(GBK)读取:" + new String(chars, 0, count)); // 正常 } fr2.close(); ``` ```java // 选做:String 编码/解码乱码演示(getBytes 编码 + new String 解码) byte[] gbkBytes = "中国".getBytes(Charset.forName("GBK")); String wrong = new String(gbkBytes, Charset.forName("UTF-8")); System.out.println(wrong); // 输出乱码(编码表不一致) String right = new String(gbkBytes, Charset.forName("GBK")); System.out.println(right); // 输出:中国 ``` ## 练习五:UserRegister ```java // 1、获取用户输入的用户名和密码(TODO 1~3) Scanner sc = new Scanner(System.in); System.out.println("请输入用户名:"); String username = sc.nextLine(); System.out.println("请输入密码:"); String password = sc.nextLine(); // 2、将用户名和密码写入到本地文件中(TODO 4~6) FileWriter fw = new FileWriter("D:/user.txt"); // 创建文件字符输出流 fw.write("用户名:" + username); // 直接写字符串,无需 getBytes fw.write("\r\n"); // 换行(Windows 是 \r\n) fw.write("密码:" + password); fw.flush(); // 刷新缓冲区,确保写入文件 // 3、关流,释放资源(TODO 7) fw.close(); System.out.println("注册成功!信息已保存到 D:/user.txt"); ``` ```java // 选做一:追加模式多用户注册(不覆盖之前数据) FileWriter fw = new FileWriter("D:/user.txt", true); // append=true 追加写 fw.write("用户名:" + username + "\r\n"); fw.write("密码:" + password + "\r\n"); fw.close(); // 选做二:FileReader 读取文件做登录校验 FileReader fr = new FileReader("D:/user.txt"); char[] chars = new char[1024]; int count; String data = ""; while ((count = fr.read(chars)) != -1) { data += new String(chars, 0, count); // 读出文件全部内容 } fr.close(); // 判断 data 是否包含 "用户名:" + username + 换行 + "密码:" + password 片段 boolean ok = data.contains("用户名:" + username + "\r\n" + "密码:" + password); System.out.println(ok ? "登录成功" : "用户名或密码错误"); ``` ## 练习六:ScoreSort ```java // 1、将文件中的数据读取到程序中(TODO 1~2):BufferedReader 逐行读取 BufferedReader br = new BufferedReader(new FileReader("D:/scores.txt")); ArrayList list = new ArrayList<>(); String line; while ((line = br.readLine()) != null) { // 2、对读取到的数据进行处理(TODO 3~4):split 拆分 + 转整数 String[] parts = line.split("-"); String name = parts[0]; int score = Integer.parseInt(parts[1]); // 3、将处理后的数据添加到集合中(TODO 5):封装 Student 对象 list.add(new Student(name, score)); } br.close(); // 4、对集合中的数据进行排序(TODO 6):Comparator 按成绩降序 Collections.sort(list, new Comparator() { @Override public int compare(Student o1, Student o2) { return o2.getScore() - o1.getScore(); // 降序:交换参数 } }); // 5、将排序后的集合中的数据写入到文件中(TODO 7~9) BufferedWriter bw = new BufferedWriter(new FileWriter("D:/sorted.txt")); for (int i = 0; i < list.size(); i++) { bw.write("第" + (i + 1) + "名:" + list.get(i)); bw.newLine(); // 跨平台换行 } bw.flush(); bw.close(); System.out.println("排序完成!结果已写入 D:/sorted.txt"); ``` ```java // 选做一:成绩相同时按姓名升序(Comparator 二次比较) Collections.sort(list, new Comparator() { @Override public int compare(Student o1, Student o2) { int result = o2.getScore() - o1.getScore(); // 先比成绩【降序】 return result == 0 ? o1.getName().compareTo(o2.getName()) : result; // 成绩相同再比姓名【升序】 } }); // 选做二:Student 实现 Comparable,TreeSet 自动排序 class Student implements Comparable { // ...属性、构造、getter、toString 省略... @Override public int compareTo(Student o) { int result = o.getScore() - this.getScore(); // 按成绩降序 return result == 0 ? this.getName().compareTo(o.getName()) : result; } } TreeSet set = new TreeSet<>(list); // 自动排序 + 去重 ``` --- # 涵盖知识点总览 | 知识点 | 说明 | |--------|------| | `CopyFile` 接口 | 统一文件复制规范,体现面向接口编程 / 多态 | | `FileInputStream` / `FileOutputStream` | 文件字节输入 / 输出流 | | `BufferedInputStream` / `BufferedOutputStream` | 缓冲流包装底层字节流,内部 8KB 缓冲区 | | 缓冲流构造方法 | `BufferedOutputStream(OutputStream out)` / `BufferedInputStream(InputStream in)`,参数是底层流 | | `byte[]` 缓冲区循环复制 | `while ((count = bis.read(bytes)) != -1) { bos.write(bytes, 0, count); }` | | `flush()` 刷新 | 缓冲区未满时手动强制写入磁盘 | | try-catch-finally | 流变量 null 声明 + finally 关闭 + `IOException` 包装 `RuntimeException` | | 关闭顺序 | 先关缓冲流再关底层流(close 缓冲流会自动关闭底层流) | | `System.currentTimeMillis()` | 复制前后毫秒数相减,做耗时对比 | | `Random` 随机字母 | `ran.nextInt(26) + 65` 生成 'A'~'Z'(65~90 是 ASCII 码) | | `read()` 返回 -1 | EOF 结束标记;`(char) b` 强转把字节值显示为字母 | | 数组统计 | `count[b - 'A']++` 用下标映射字母,统计频次并找出最大值 | | `FileReader` / `FileWriter` | 文件字符输入 / 输出流;字符流 = 字节流 + 字符集编码表 | | `write(String)` 直接写中文 | 字符流可直接写字符串,无需 `getBytes()` 转换 | | `char[]` 字符缓冲区 | `fr.read(chars)` 批量读字符;`new String(chars, 0, count)` 转字符串 | | `Charset` 字符集 | `FileWriter(String, Charset)` / `FileReader(String, Charset)` 构造指定编码 | | String 编码 / 解码 | `getBytes(Charset)` 字符串 → 字节(编码);`new String(byte[], Charset)` 字节 → 字符串(解码) | | 乱码原理 | 读写字符集不一致 → 乱码;GBK 汉字 2 字节,UTF-8 汉字 3 字节 | | Scanner 键盘录入 | `sc.nextLine()` 获取用户名、密码输入(配合 `FileWriter` 完成数据持久化) | | 数据永久化存储 | 内存数据程序结束即丢失;写入本地文件(磁盘)即可永久保存 | | `FileWriter` 追加模式 | `new FileWriter(String, true)` append=true 追加写,不覆盖原文件内容 | | 登录校验(选做) | `FileReader` 读文件 + `String.contains()` 判断用户名密码是否匹配 | | `BufferedReader` / `BufferedWriter` | 字符缓冲流包装 `FileReader` / `FileWriter`,提供高效读写 | | `readLine()` 逐行读取 | 读取一行(不含终止符),到文件末尾返回 null;`while ((line = br.readLine()) != null)` | | `newLine()` 写入换行 | 按系统属性写行分隔符(跨平台,替代手写 `\r\n`) | | `String.split()` 拆分 | `line.split("-")` 按分隔符把一行拆成 String[],用于解析"姓名-成绩" | | `Integer.parseInt()` | 字符串 → 整数转换,排序时才能按数值比较 | | 集合 + 排序 | `ArrayList` 存储 `Student` 对象 + `Collections.sort` + `Comparator` 匿名内部类按成绩降序 | | 读 → 处理 → 集合 → 排序 → 写回 | 文件数据处理五步闭环:字符缓冲流读 → 处理 → 集合 → 排序 → 字符缓冲流写回 |