20260807-IO缓冲流练习.md 32 KB

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(缓冲流包装底层流)。现在把两个实现整合进一个对比程序,复制同一个大文件,用耗时说话——验证"缓冲流更快"。

代码框架

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 读回 → 统计每个大写字母出现的次数 → 输出出现最多的字母。

代码框架

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 读回并显示到控制台,实现"字符流三步走"的完整闭环。

代码框架

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 字符集读取 → 中文正常显示。

代码框架

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. 关流,释放资源

代码框架

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

代码框架

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<Student>() { ... });
        //         按成绩【降序】: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<Student>(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

// 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 "文件复制成功";
// 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 "复制成功";
// 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

// 写(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

// 写(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();
// 选做: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

// 写(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();
// 选做: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

// 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");
// 选做一:追加模式多用户注册(不覆盖之前数据)
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

// 1、将文件中的数据读取到程序中(TODO 1~2):BufferedReader 逐行读取
BufferedReader br = new BufferedReader(new FileReader("D:/scores.txt"));
ArrayList<Student> 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<Student>() {
    @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");
// 选做一:成绩相同时按姓名升序(Comparator 二次比较)
Collections.sort(list, new Comparator<Student>() {
    @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<Student> {
    // ...属性、构造、getter、toString 省略...
    @Override
    public int compareTo(Student o) {
        int result = o.getScore() - this.getScore();                   // 按成绩降序
        return result == 0 ? this.getName().compareTo(o.getName()) : result;
    }
}
TreeSet<Student> 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 匿名内部类按成绩降序
读 → 处理 → 集合 → 排序 → 写回 文件数据处理五步闭环:字符缓冲流读 → 处理 → 集合 → 排序 → 字符缓冲流写回