c260807course在文件复制的场景中,先定义一个 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接口类型,具体用哪种复制实现可以随时替换(多态)
// 来源:course/Demo01.java(注释部分)
/*
字节缓冲流
BufferedOutputStream:这个类是实现缓冲输出流的类,通过设置这样的输出流,程序可以向底层输出流写入字节,
也就是说它针对的不是真正的文件,而是文件的输出流。
BufferedInputStream:这个类是实现缓冲输入流的类。通过设置输入流,可以向底层输入流读取数据。
这个类里面维护了一个字节缓冲区,内部缓冲区会根据需要,从包含的输入流中获取数据填充到缓冲区。一次可以读取多个字节。
public BufferedOutputStream(OutputStream out) 缓冲字节输出流的构造方法,参数是字节输出流
public BufferedInputStream(InputStream out) 缓冲字节输入流的构造方法,参数是字节输入流
*/
FileOutputStream 包装起来,先写入内部缓冲区,满了再统一刷到磁盘read() 直接从内存缓冲区取,减少与磁盘的交互次数FileOutputStream / FileInputStream,再用缓冲流把它们包装起来要点:缓冲流本质是"包装流 / 装饰流"——它不直接操作文件,而是给已存在的字节流"加一层缓冲",牺牲少量内存换取大幅减少磁盘 I/O 次数,从而显著提升读写效率。这正对上节课"byte[] 缓冲区循环复制"的思想:缓冲 = 一次操作一批数据,而非一个字节一个字节。
// 来源:course/Demo01.java(注释部分,本次更新新增)
/*
FileInputStream和FileOutputStream属于基础流,他们是直接接触和操作文件的。
BufferedOutputStream和BufferedInputStream属于扩展流,他们是用来操作基础流的。
*/
要点:
- 基础流(FileInputStream / FileOutputStream 等)直接接触和操作文件,是数据的"源头与终点"
- 扩展流(BufferedOutputStream / BufferedInputStream 等)不直接操作文件,而是用来"操作基础流"的——在基础流之上叠加缓冲、转换等增强能力
- 这个"基础流 + 扩展流"的分工,正是后续所有高级流(字符流、转换流、对象流)的共同组织方式:先建基础流,再用扩展流包装它
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 消化掉),属于"方法内部自处理"风格
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 的"内部消化"形成对照——接口允许两种实现风格并存
// 来源: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'
// 来源: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()关闭流——这是教学演示,实际开发中流必须关闭,否则文件句柄泄漏、数据可能未刷入磁盘
// 来源: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),对比哪个更快——缓冲流通常显著占优
由于字节流操作字符(特别是中文)不方便,所以 Java 提供了字符流。字符流的本质公式:字符流 = 字节流 + 字符集编码表。
// 来源:course/Demo02.java(注释部分)
/*
由于字节流操作字符(特别是中文)不方便,所以Java提供了字符流
字符流 = 字节流 + 字符集编码表
*/
字符集编码表是一个系统支持的所有字符的集合,包括各个国家的文字、符号、图像符号、数字等等。计算机要识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。
常见的字符编码:ASCII、GB-2312、GBK、Unicode(UTF-8)。
// 来源:course/Demo02.java(注释部分)
/*
什么是字符集编码表?
是一个系统支持的所有的字符的集合 ,包括各个国家的文字、符号、图像符号、数字等等
计算机要像识别这些文字和符号,就需要存储一套编码表,然后按照表中的对应关系,存储对应的字符编码。
常见的字符编码:ASCII、GB-2312、GBK、Unicode(UTF-8)
*/
// 来源: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) // 使用指定的字符集名称解码
要点:编码是字符串 → 字节,解码是字节 → 字符串;同一段字节用不同编码表解码会得到不同结果(乱码问题的根源)。
与字节流(InputStream / OutputStream)对称,字符流也分输入、输出两大体系,子类名同样以父类名作后缀:
字符流:
|- 输入字符流:Reader 是输入字符流的抽象类
|- 文件输入字符流:FileReader
|- 输出字符流:Writer 是输出字符流的抽象类
|- 文件输出字符流: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),解决不同编码文件的乱码问题
// 来源: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() 关闭流
// 来源: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()之前会自动刷新一次缓冲区
字符缓冲流是字符流(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
*/
// 来源: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(...)))
Demo04 是本日 IO 流知识的小结,把今天学的所有流按"基础流 / 处理流(扩展流)"和"字节 / 字符"两个维度归类,形成完整体系:
// 来源:course/Demo04.java(注释部分)
/*
基础流两种:
字节流: 处理的是可以任意类型的文件
输出字节流 OutputStream
文件输出字节流:FileOutputStream
输入字节流 InputStream
文件输入字节流:FileInputStream
字符流: 处理的是纯文本文件
输入字符流 Reader
文件输入字符流:FileReader
输出字符流 Writer
文件输出字符流:FileWriter
处理流(扩展流)-缓冲流两种:
缓冲字节流: 处理的是字节输入流和字节输出流。
缓冲字节输入流:BufferedInputStream
缓冲字节输出流:BufferedOutputStream
缓冲字符流: 处理的是字符输入流和字符输出流。
缓冲字符输入流:BufferedReader
缓冲字符输出流:BufferedWriter
*/
┌─ 字节流(处理任意类型文件)
│ ├─ 输出字节流 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)
- 选型口诀:不确定类型用字节流;纯文本用字符流;要效率再加对应的缓冲流
用 BufferedOutputStream 把 100 万随机大写字母写入 D:/letters.txt(每 500 个换行并 flush),再用 BufferedInputStream 逐字节读回,统计每个大写字母出现次数,并找出出现最多的字母。
// 来源: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();
// 来源: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))一行完成"创建文件流 + 缓冲流包装"
实体类 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()输出"姓名-成绩"
本课知识点围绕"更快更规范地复制文件"展开,一张表看清"知识 → 代码位置":
| 知识点 | 具体体现 | 源码位置 |
|---|---|---|
| 接口定义(契约) | 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 |
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() 为什么能"一次读一行"D:/scores.txt 中"姓名-成绩"乱序,用 BufferedReader 逐行读 + split("-") 拆分 + ArrayList 存 Student 对象 + Collections.sort(Lambda)按成绩降序 + BufferedWriter 写回 D:/sorted.txt 带名次结果Demo04 小结,自己画出今日 IO 流的完整体系图(基础流:字节/字符 + 输入/输出;处理流:缓冲字节/缓冲字符),并说出每个流"处理什么类型、怎么配对包装"java.io.BufferedInputStream、java.io.BufferedOutputStream、java.io.Reader、java.io.Writer、java.io.FileReader、java.io.FileWriter、java.io.BufferedReader、java.io.BufferedWritertry (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() 用于"不关闭流但需要及时看到数据"的场景FileReader / FileWriter 构造时用 Charset 显式指定编码可避免