c260808course / homework0807上一日学习的字符流使用平台默认字符集读写文件。但现实中会遇到这种情况:一个文件本身是 GBK 编码的,而我们手头只能使用 UTF-8 字符集,想要给这个文件追加写入内容——直接写必然乱码。这时就需要转换流:把 A 字符集的文件,转换为 B 字符集的文件。
// 来源:course/Demo01.java(注释部分)
/*
我们讲字符流的时候,说过字符集的概念,
不同的字符集虽然都能转换成字节(二进制数)但是,同一个字节,在不同的字符集中表示的含义是不一样的。
字符--编码-->字节--解码-->字符
如果使用字符集A进行编码,使用字符集B进行解码,就会出现字符集乱码。
*/
字符 --编码--> 字节 --解码--> 字符
要点:
- 编码(Encoding):字符 → 字节(字符集编码表负责把文字符号转成二进制数)
- 解码(Decoding):字节 → 字符(把二进制数按编码表还原成文字符号)
- 乱码根源:不同字符集虽然都能把字符转成字节,但同一个字节在不同字符集中表示的含义不一样——用字符集 A 编码、用字符集 B 解码,就会得到错误字符(乱码)
- 转换流的应用场景:把 GBK 编码的文件内容转成 UTF-8(或反之),实现不同字符集文件之间的转换
InputStreamReader 是字节流到字符流的桥梁,父类是 Reader。它读取字节,并使用指定的字符集把它解码为字符。使用的字符集可以按名称指定,也可以直接使用平台默认字符集。
// 来源:course/Demo01.java(注释部分)
/*
InputStreamReader:是字节流到字符流的桥梁,父类是Reader
他是读取字节,并且使用指定的字符集把它解码为字符。
它使用的字符集可以有名称指定,也可以直接使用平台默认字符集。
构造方法:
public InputStreamReader(InputStream in) 使用默认字符集创建InputStreamReader对象
public InputStreamReader(InputStream in, String charsetName) 使用charsetName指定的字符集创建InputStreamReader对象
*/
要点:
- 角色定位:字节流 → 字符流的"桥",本质上是一个 Reader,但它底层吃的是字节
- 核心能力:读取底层字节流时按指定字符集解码成字符,解决"用 A 字符集编码的字节、用 A 字符集读取"的配对问题
- 构造方法两种:不传字符集用平台默认;传字符集名称(如
"UTF-8"、"GBK")则按指定字符集解码
OutputStreamWriter 是字符流到字节流的桥梁,父类是 Writer。它读取字符,使用指定的字符集把它编码为字节,再交给底层字节流写入。
// 来源:course/Demo01.java(注释部分)
/*
OutputStreamWriter:是字符流到字节流的桥梁,父类是Writer
它是读取字符,使用指定的字符集把它编码为字节。
构造方法:
public OutputStreamWriter(OutputStream out) 使用默认字符集创建OutputStreamWriter对象
public OutputStreamWriter(OutputStream out, String charsetName) 使用charsetName指定的字符集创建OutputStreamWriter对象
*/
| 桥梁 | 父类 | 方向 | 动作 | 底层参数 | 典型使用 |
|---|---|---|---|---|---|
InputStreamReader |
Reader |
字节 → 字符 | 解码 | InputStream |
读取文件字节按指定字符集解码为字符 |
OutputStreamWriter |
Writer |
字符 → 字节 | 编码 | OutputStream |
字符内容按指定字符集编码为字节写入文件 |
要点:
- 命名规律:
XxxReader是输入(读)、XxxWriter是输出(写),与 FileReader / FileWriter 命名一致- 构造参数是字节流:
new InputStreamReader(new FileInputStream(...), "UTF-8")—— 先用文件字节流打开文件,再包上转换流,并指定字符集- 与字符流的关系:字符流(FileReader/FileWriter)本质上就是"字节流 + 默认字符集",而转换流允许手动指定字符集,更加灵活
使用 UTF-8 字符集打开 abc.txt 文件,并把内容转换成 GBK 字符集写入到 hello.txt 文件。
// 来源:course/Demo01.java
public static void main(String[] args) throws IOException {
// 使用UTF-8字符集打开abc.txt文件(读端:字节→字符,按UTF-8解码)
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\abc.txt"), "UTF-8");
// 把内容使用GBK字符集写入到hello.txt文件(写端:字符→字节,按GBK编码)
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\hello.txt"), "GBK");
char[] chars = new char[1024];
int count;
while ((count = isr.read(chars)) != -1) { // 按 UTF-8 解码读出字符
osw.write(chars, 0, count); // 按 GBK 编码写出字符
}
osw.close();
isr.close();
}
D:\abc.txt(UTF-8字节) → FileInputStream → InputStreamReader(UTF-8解码) → char[]
↓
char[] → OutputStreamWriter(GBK编码) → FileOutputStream → D:\hello.txt(GBK字节)
要点:
- 读写两端各自指定字符集:读端
InputStreamReader指定 UTF-8(源文件编码),写端OutputStreamWriter指定 GBK(目标文件编码),完成"转码"- 与字符缓冲流相同的循环读法:
char[1024]数组 +while ((count = isr.read(chars)) != -1)批量读写(对应字符流的 read(char[]) 批量读)- 转换流属于扩展流(处理流):它"处理"的是底层字节流(FileInputStream / FileOutputStream),符合上一日总结的"基础流 + 扩展流"组织方式
- 方法抛 IOException:main 直接
throws IOException,由 JVM 处理(教学简化写法)
类文件(.java / .class)可以分享给其他人,但根据类创建的对象是在程序运行时于内存中开辟空间创建的,无法直接分享。通过对象持久化,可以把内存中的对象保存到磁盘,或者在网络中传输对象,从而分享给他人。
// 来源:course/Demo02.java(注释部分)
/*
我们可以把一个类文件分享给其他人,但是如果是根据某个类创建的对象,想要分享给其他人,就不容易了
因为它是程序运行时,在内存中开辟空间创建的对象。
但是我们可以通过【对象持久化】的方式,把内存中的对象,保存到磁盘中,分享其他人。
对象持久化:就是把对象保存到磁盘、或者在网络中传输对象。
这种机制就是使用一个字节序列表示一个对象,这个字节序列包含:
对象的类型
对象的数据,和对象中存储的属性等等信息。
把字节序列写入到文件中后,就相当于永久的保存了一个对象。
反过来,可以使用【对象反序列化】的方式再从文件中读取出对象,进行对象重构。
*/
序列化(保存):对象(内存) --对象序列化流 ObjectOutputStream--> 字节序列 --> 磁盘文件(永久保存)
反序列化(读取):磁盘文件 --对象反序列化流 ObjectInputStream--> 字节序列 --> 内存中重新构造对象
要点:
- 对象持久化:把内存中的对象保存到磁盘、或在网络中传输对象——本质是把"对象"变成字节序列
- 字节序列的内容:包含对象的类型、对象的数据、对象中存储的属性等信息,所以反序列化时能完整还原对象
- 序列化:对象 → 字节序列 → 文件,永久保存对象
- 反序列化:从文件读取字节序列 → 在内存中重构对象
- 类比:序列化像"拍快照存档",反序列化像"读档恢复"
进行对象序列化的方式是使用对象序列化流 ObjectOutputStream,它把对象写入到字节输出流中:
// 来源:course/Demo02.java(注释部分)
/*
进行对象序列化的方式就是使用【对象序列化流】 ObjectOutputStream
构造方法:
public ObjectOutputStream(OutputStream out) 创建一个写入指定OutputStream的对象序列化流对象
对象序列化的方法:
public final void writeObject(Object obj) 将指定对象obj写入到ObjectOutputStream
*/
// 来源:course/Demo02.java(注释部分)
/*
一个对象想要被序列化,必须要实现一个接口 java.io.Serializable
Serializable接口没有任何需要重写的方法,也没有任何属性,它就像一个标识,标志着这个类的对象可以被序列化。
*/
要点:
- 构造方法:
new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt"))—— 同样先用文件字节输出流,再包上对象流- 核心方法:
writeObject(Object obj)把任意对象写入流- Serializable 是标记接口(标识接口):没有任何抽象方法、没有任何属性,它像一个"通行证标识",标志该类的对象可以被序列化——凡是支持序列化的类都必须实现它
ObjectInputStream 把对象序列化后的文件进行反序列化,返回对象:
// 来源:course/Demo02.java(注释部分)
/*
进行对象反序列化的方式就是使用【对象反序列化流】 ObjectInputStream
ObjectInputStream是把对象序列化后的文件进行反序列化,返回对象。
public ObjectInputStream(InputStream in) 创建从InputStream中读取的反序列化对象
public final Object readObject() 从ObjectInputStream读取一个对象
*/
要点:
- 构造方法:
new ObjectInputStream(new FileInputStream("D:/studentObject.txt"))读取序列化文件- 核心方法:
readObject()返回Object类型——需要强转为实际类型才能使用- 编译期异常:
readObject()会抛ClassNotFoundException(找不到对应类时),所以 main 要throws IOException, ClassNotFoundException
// 来源:course/Demo02.java(注释部分)
/*
如果 第1天 张三,通过Student类,创建s对象,使用对象序列化把对象持久化保存,并且发送给李四。
李四收到序列化的文件。
张三 修改了Student类,修改了类的属性,比如增加了一个sex性别,移除了age年龄。
李四再进行反序列化,会得到什么?
答:会报错,抛异常:java.io.InvalidClassException
怎么解决?
方案一:重新序列化
方案二:为对象所属的类增加一个属性:
private static final long serialVersionUID =42L;
完成,再进行序列化后,修改属性,再反序列化的时候,就不会报错抛异常,而是正常显示,
有这个属性,就正常赋值,没有就赋值默认值,比如:
Student{id=1001, name='张三', age=null}
*/
| 方案 | 说明 |
|---|---|
| 重新序列化 | 类变了就用新类重新创建对象、重新保存,但丢失了旧数据 |
固定 serialVersionUID |
在类中显式声明 private static final long serialVersionUID = 42L;,类结构变化时也能正常反序列化 |
要点:
- InvalidClassException:序列化时类含隐式生成的版本号,反序列化时如果类的结构(属性)变了,JVM 认为"版本不一致"直接抛异常
- serialVersionUID 的作用:显式固定类的"版本号",序列化 / 反序列化两端版本号一致即可兼容;类属性变了,有这个属性就正常赋值,没有就赋默认值(如
age=null)- 推荐习惯:凡是实现 Serializable 的类,都显式声明 serialVersionUID,避免类升级导致旧数据无法读取
代码更新后,Demo02 补充了 serialVersionUID 的底层机制说明——为什么类结构变化会导致反序列化失败:
// 来源:course/Demo02.java(注释部分,本次更新新增)
/*
serialVersionUID的作用:
1、用来验证反序列化的时候类的版本一致性
2、如果我们没有自己定义这个属性,那么JVM会根据类型的结构自己定义serialVersionUID。
3、如果JVM定义serialVersionUID,然后类的结构发生了改变,就会导致serialVersionUID值也发生变化。
Student类--JVM假设 定义的serialVersionUID是123456L,序列化对象,对象的serialVersionUID也是123456L
Student类的结构发生了改变,serialVersionUID值也会发生改变,比如变为 456789L。
再进行反序列化的时候,对比两个serialVersionUID值发现不一致,就会出现异常java.io.InvalidClassException
最好的方式就是我们自己显式声明serialVersionUID属性,至于是什么值,无所谓。
private static final long serialVersionUID =42L;
*/
要点:
- 作用 1——版本一致性校验:
serialVersionUID用来验证反序列化时类的版本是否一致(序列化端与反序列化端是否为同一个版本)- 作用 2——JVM 自动生成:如果我们没有自己定义这个属性,JVM 会根据类的结构自动生成一个
serialVersionUID- 作用 3——类结构变化导致版本号漂移:由 JVM 生成的版本号与类结构强相关——类结构一变,自动版本号就变(如
123456L → 456789L);反序列化时对比两端版本号不一致,就抛InvalidClassException- 最佳实践:自己显式声明
serialVersionUID,至于是什么值无所谓(任意 long),只要序列化 / 反序列化两端一致即可——这就是"显式声明版本号能免疫类结构变化"的根本原因
如果类中某个属性不想要被序列化(例如敏感信息、临时数据),可以给该成员加 transient 关键字修饰,序列化时这个属性不会参与序列化:
// 来源:course/Demo02.java(注释部分)
/*
如果类中有某个属性不行被序列化,可以给这个成员添加transient关键字修饰:
private transient Integer id;
那么在序列化的时候,这个属性。就不会参与序列化。
*/
要点:
- transient 修饰成员变量:
private transient Integer id;—— 序列化时跳过该属性- 反序列化结果:被 transient 修饰的属性,反序列化后为默认值(引用类型为 null、基本类型为 0/false)
- 典型用途:密码、缓存、与对象状态无关的临时数据等不宜持久化的字段
代码更新后,Demo02 补充了序列化的注意事项,总结了使用对象序列化时必须注意的 6 条规则:
// 来源:course/Demo02.java(注释部分,本次更新新增)
/*
序列化的注意事项:
1、静态属性不能序列化,因为静态属性是属于类的,不是属于某个对象的。
2、transient修饰的属性不能序列化
3、父类实现了java.io.Serializable接口,那么子类可以自动序列化。
4、反序列化不会调用构造方法
5、如果对象引用了其他的对象,那么引用链上的所有的对象必须是可以序列化的。
6、单例对象需要重写一个方法readResolve()方法
*/
| # | 注意事项 | 解读 |
|---|---|---|
| 1 | 静态属性不能序列化 | static 成员属于类、不属于某个对象,序列化的是"对象"的字段,所以静态属性天然被排除在外 |
| 2 | transient 属性不能序列化 | 与第 9 节呼应:transient 修饰的成员变量序列化时被跳过,反序列化为默认值 |
| 3 | 父类实现 Serializable → 子类自动可序列化 | 继承关系中,父类实现了 Serializable,子类无需重复实现即可被序列化(子类自动继承了"可序列化"能力) |
| 4 | 反序列化不会调用构造方法 | readObject() 是按字节序列在内存中直接重构对象,不走构造方法(也不走实例初始化代码块)——这也是第 6 条单例问题的根源 |
| 5 | 引用链上所有对象必须可序列化 | 对象引用其他对象时,序列化会把引用链上的对象一并序列化;链上任一对象未实现 Serializable,就抛 NotSerializableException |
| 6 | 单例对象需重写 readResolve() |
单例类被反序列化时,会绕过 private 构造器直接重构出另一个实例,破坏单例;重写 readResolve() 返回既有单例,可保持全局唯一 |
要点:
- 第 1、2 条是"哪些成员不参与序列化":
static(属于类)+transient(显式排除)——这也是 08-07 拓展阅读中"static 本身就不参与序列化"的正式总结- 第 4 条是理解"反序列化为什么能绕过构造器"的关键:对象重构完全依赖字节序列,不经过任何构造逻辑
- 第 5 条常见报错:
NotSerializableException会点名是引用链上哪个类没实现 Serializable,排查时顺着引用链逐级检查即可- 第 6 条的
readResolve()是单例 + 序列化的经典组合:重写后反序列化返回的仍是内存中唯一的那个实例
Demo03 是本日(乃至整个 IO 流阶段)知识的总收口:把之前学过的所有 IO 流按"基础流 / 处理流"两大维度完整归类,并汇总了常用方法、特殊方法、特殊属性、特殊关键字:
// 来源:course/Demo03.java(本次更新新增文件)
/*
基础流:
字节流:
字节输入流:InputStream
|-文件字节输入流:FileInputStream
字节输出流:OutputStream
|-文件字节输出流:FileOutputStream
字符流:
字符输入流:Reader
|-文件字符输入流:FileReader
字符输出流:Writer
|-文件字符输出流:FileWriter
处理流:
缓冲流:
缓冲字节流:BufferedInputStream / BufferedOutputStream
缓冲字符流:BufferedReader / BufferedWriter
转换流:
输入:InputStreamReader---把字节流根据指定的字符集转换为字符
输出:OutputStreamWriter--把字符根据指定的字符集转化为字节流
对象流:
序列化:ObjectOutputStream
反序列化:ObjectInputStream
*/
┌─ 字节流(处理任意类型文件)
│ ├─ 输入:InputStream → FileInputStream
│ └─ 输出:OutputStream → FileOutputStream
基础流 ───────────────┤
│ ├─ 输入:Reader → FileReader
└─ 字符流(处理纯文本文件)
└─ 输出:Writer → FileWriter
├─ 缓冲流(给字节/字符流加缓冲)
│ ├─ BufferedInputStream / BufferedOutputStream
│ └─ BufferedReader / BufferedWriter
IO 流 ─── 处理流 ──────────────────────┤
├─ 转换流(字节 ↔ 字符 + 指定字符集)
│ ├─ 输入:InputStreamReader(字节→字符,解码)
│ └─ 输出:OutputStreamWriter(字符→字节,编码)
│
└─ 对象流(对象持久化 / 序列化)
├─ 序列化:ObjectOutputStream
└─ 反序列化:ObjectInputStream
// 来源:course/Demo03.java(注释部分)
/*
常用的方法:
write() 把内存中的数据写入到文件
read() 从文件中读取数据到内存
flush() 强制刷新缓冲流缓冲区的数据到文件
close() 关闭流,释放资源
特殊方法:
readLine() 读取一行数据,缓冲输入字符流特有的
newLine() 根据不同的系统进行一次换行。
writeObject() 对象序列化--把内存中的对象保存到文件中
readObject() 反序列化--从文件中读取并在内存中重构对象,不走构造方法。
特殊属性:
private static final long serialVersionUID 序列化的版本UID,用来验证反序列化和序列化的类版本一致性
特殊的关键字:
transient 修饰属性,让属性不参与对象序列化
*/
要点:
- 四大流家族齐了:缓冲流(08-07)、字符流 / 字符缓冲流(08-07)、转换流(本日)、对象流(本日)全部归入"处理流",共同特点是包装底层基础流、增加增强能力
- 命名规律总结:文件流子类以父类名作后缀(FileInputStream / FileReader...);缓冲流以
Buffered前缀命名;转换流是InputStreamReader / OutputStreamWriter;对象流是ObjectInputStream / ObjectOutputStream- 四个通用方法:
write()/read()/flush()/close()是所有流共用的基本功- 四组特殊方法:
readLine()/newLine()(缓冲字符流特有)、writeObject()/readObject()(对象流特有)——readObject()重提"不走构造方法"(对应第 10 节第 4 条)- 一个特殊属性 + 一个特殊关键字:
serialVersionUID(版本一致性)+transient(跳过序列化)——对象流专属的"配置项"
Student 类是演示对象序列化的实体类,集中体现了 serialVersionUID、transient、Serializable、equals/hashCode/toString 等知识点:
// 来源:course/Student.java
public class Student implements Serializable {
// 增加一个属性:固定序列化版本号(类结构变化也能兼容反序列化)
private static final long serialVersionUID = 42L;
// transient:id 不参与序列化(反序列化后为 null)
private transient Integer id;
private String name;
private Integer age;
// private String sex; // 注释掉的属性:演示"类结构变化"场景
public Student() { }
public Student(Integer id, String name) {
this.id = id;
this.name = name;
this.age = age;
}
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Student student)) return false;
return Objects.equals(id, student.id)
&& Objects.equals(name, student.name)
&& Objects.equals(age, student.age);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
要点:
implements Serializable:类实现标记接口,允许对象被序列化(本类头注释明确说明)serialVersionUID = 42L:显式固定版本号,类属性增删后旧数据仍可反序列化(对应 Demo02 的经典场景)private transient Integer id:id 不参与序列化,反序列化后为 null(可在控制台打印Student{id=null, ...}验证)sex属性被注释:与 Demo02 注释中"增加 sex、移除 age"的场景呼应,演示类结构变化对反序列化的影响- equals / hashCode / toString 重写:用
Objects.equals/Objects.hash工具方法实现(前面课程学过的 Object 类知识点复用)
// 来源:course/ObjectOutputStreamTest.java(活动代码已注释保留)
public static void main(String[] args) throws IOException {
// 创建对象
Student s = new Student(1001, "张三", "男");
// 序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt"));
// 调用方法,进行序列化---把对象写入到文件中
oos.writeObject(s);
// 释放资源
oos.close();
}
// 来源:course/ObjectInputStreamTest.java
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 创建反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/studentObject.txt"));
// 调用方法进行反序列化
Object o = ois.readObject();
System.out.println(o); // 输出 Student{...},依赖重写的 toString()
}
1、创建 Student 对象(内存)
2、new ObjectOutputStream(文件字节流) → writeObject(s) → 序列化到 D:/studentObject.txt
3、new ObjectInputStream(文件字节流) → readObject() → 反序列化回内存
4、System.out.println(o) → 调用重写的 toString() 打印对象内容
要点:
- 写端注意:
new Student(1001, "张三", "男")是演示代码,当前 Student 的构造器是Student(Integer, String)两个参数,演示时需匹配(代码已注释保留)- 读端返回 Object:
readObject()返回Object类型,实际使用时需强转成 Student:Student s = (Student) ois.readObject();- 两类异常:写端抛
IOException,读端还需抛ClassNotFoundException(readObject 找不到类时抛出)- 输出依赖 toString():
System.out.println(o)打印对象会调用 Student 重写的toString(),得到Student{id=1001, name='张三', age=null}这样的可读格式
homework0807/Demo_03.java 是 8 月 7 日课后作业第三题(接口化文件复制效率对比器),把 8 月 7 日学过的"接口化设计 + 字节缓冲流"做了一次综合应用,与本日的"扩展流(处理流)"体系呼应。
// 来源:homework0807/Demo_03.java
interface CopyFile {
String copy(String src, String desc) throws IOException;
}
// 来源:homework0807/Demo_03.java
static 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 len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
return "文件复制失败";
} finally {
try {
fos.close();
fis.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return "文件复制成功";
}
}
// 来源:homework0807/Demo_03.java
static class BufferedCopy implements CopyFile {
@Override
public String copy(String src, String desc) throws IOException {
// 先创建文件流,再用缓冲流包装
FileOutputStream fos = new FileOutputStream(desc);
FileInputStream fis = new FileInputStream(src);
BufferedOutputStream bos = new BufferedOutputStream(fos);
BufferedInputStream bis = new BufferedInputStream(fis);
byte[] bytes = new byte[8192];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
// 先关缓冲流再关底层流
bos.close();
bis.close();
fos.close();
fis.close();
return "复制成功";
}
}
// 来源:homework0807/Demo_03.java
public static void main(String[] args) throws IOException {
String src = "D:\\CentOS7\\kyl-CentOS7.zip"; // 换成任意大于 10MB 的文件
// 普通字节流复制
long start1 = System.currentTimeMillis();
new Copy01().copy(src, "D:/copy1.zip");
long end1 = System.currentTimeMillis();
System.out.println("耗时:" + (end1 - start1));
// 缓冲流复制
long start2 = System.currentTimeMillis();
new BufferedCopy().copy(src, "D:/copy2.zip");
long end2 = System.currentTimeMillis();
System.out.println("耗时:" + (end2 - start2));
// TODO 10: 输出两次耗时,对比哪个更快
}
要点:
- 接口 + 两个实现:CopyFile 接口定义规范,Copy01(普通字节流)与 BufferedCopy(缓冲流)两种实现,main 中分别 new 并调用——面向接口编程 / 多态复用(回顾 08-07 知识点)
- 异常处理两种风格对照:Copy01 用 try-catch-finally 内部消化并返回提示字符串;BufferedCopy 直接
throws IOException向上抛出——接口允许两种风格并存- 缓冲流包装流程:先建文件流 → 缓冲流包装 → 操作缓冲流 → 先关缓冲流再关底层流(复习 08-07)
- 耗时对比:
System.currentTimeMillis()前后相减,用同一个大文件对比两种复制实现的效率- 接口成员默认 public:
interface CopyFile { ... }内嵌套定义在类中,方法默认public abstract
代码更新后,c260808 新增了多线程主题。首先理解三个基础概念:程序、进程、线程。
// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
什么线程?什么叫进程?
我们知道,程序本质上就是一种文件,它是存储在硬盘中,程序想要被执行,就需要被加载到内存中执行。
在内存中执行的着的程序就是进程。
一个程序运行起来后,就成了进程,需要占用CPU、内存、网络、GPU、磁盘等等....
一个进程中可能包含多个线程。
比如:360安全卫士,同时进行木马查杀、电脑清理、系统修复、优化加速,每使用一个功能,就是启动一个线程。
*/
| 概念 | 说明 |
|---|---|
| 程序 | 本质是存储在硬盘中的文件,是一段静态的代码 |
| 进程 | 在内存中正在执行的程序;占用 CPU、内存、网络、GPU、磁盘等资源 |
| 线程 | 进程中的单个顺序控制流,是一条执行路径;一个进程可能包含多个线程 |
要点:
- 一个程序运行起来后就成了进程,需要占用各种硬件资源(CPU / 内存 / 网络 / GPU / 磁盘)
- 一个进程中可能包含多个线程——例如 360 安全卫士同时进行木马查杀、电脑清理、系统修复、优化加速,每个功能就是一个线程
- 进程三大特性:独立性(能独立运行、系统分配资源和调度的独立单位)、动态性(进程本质是程序的一次执行过程,动态产生、动态消亡)、并发性(任何进程都可以和其他进程一起并发执行)
- 单线程 vs 多线程:一个进程只有一条路径就是单线程程序;有多条路径就是多线程程序
- main 主线程:
main方法被称为主方法,也就是 Java 程序的主线程(Hello.java中的main就是主线程的入口)
// 来源:course/Demo04.java(注释部分)
/*
线程的并行和并发
并行:同一个时刻,有多个指令在多个CPU上同时执行。比如:饭店后厨,有3个灶台被三位厨师使用炒菜。
并发:同一时刻,有多个指令在单个CPU上交替执行。比如:饭店后厨,有3个灶台,被一位厨师炒菜。
*/
| 概念 | 含义 | 后厨类比 |
|---|---|---|
| 并行(Parallel) | 同一个时刻,多个指令在多个 CPU 上同时执行 | 3 个灶台被 3 位厨师同时使用炒菜 |
| 并发(Concurrent) | 同一时刻,多个指令在单个 CPU 上交替执行 | 3 个灶台被 1 位厨师轮番炒菜 |
要点:
- 并行靠硬件:需要多个 CPU 核心同时工作,是"真·同时"
- 并发靠调度:单核 CPU 在极短时间内快速切换执行多个线程,宏观上"像同时在跑",微观上是交替执行
- 多线程的意义:从软硬件上实现多个线程并发执行,充分利用 CPU,提升性能——这也解释了为什么"多线程 ≠ 一定快"(单核场景下是并发切换)
Demo04 还顺带对比了 CPU / GPU 的特性差异,帮助理解"多核 / 多线程"的硬件背景:
// 来源:course/Demo04.java(注释部分)
/*
CPU 计算机核心
红色警戒 没有GPU,全靠CPU
特性 适合进行复杂运算,但是不适合进行简单的重复性大量运算
GPU 显卡核心
比特币挖矿 CPU不适合挖矿,比特币本质上就是一种计算机运算,
运算难度不高,复杂程度也不高,但是量大。
3A游戏,只狼、荒野大镖客、GTA5、MC
AI 大模型--->GPU
本地部署AI大模型,也是吃GPU
*/
要点:
- CPU(计算机核心):适合复杂运算,但不适合简单、重复、量大的运算(如挖矿)
- GPU(显卡核心):适合大量简单并行运算——比特币挖矿、3A 游戏渲染、本地部署 AI 大模型都吃 GPU
- 这个对比呼应"多线程"的性能本质:不同特性的任务交给不同核心处理,才能发挥硬件的并发能力
Demo04 给出了 Java 实现多线程的方式一:继承 Thread 类,重写 run() 方法,共 4 个步骤:
// 来源:course/Demo04.java(注释部分)
/*
Java中实现多线程的方式:
方式一: 继承 Thread 类,重写 run()方法。
实现步骤:
1、自定义一个类,继承Thread类
2、在自定义类中重写父类的run()方法,设置在线程中要执行的操作。
3、创建自定义线程类的对象
4、调用自定义线程类的start()方法 启动线程。
*/
// 来源:course/Demo04.java(注释部分)
/*
为什么要重写 run()方法?
因为run()方法是用来封装被线程执行的代码。
run()方法和start()方法有什么区别?
run()方法是用来封装被线程执行的代码,如果直接调用run(),不会创建新的线程,而是和普通方法一样的调用。
start()方法是启动线程的方法,调用这个方法后会启动一个新的线程,然后在线程中由JVM调用这个线程的run()方法
*/
| 方法 | 作用 | 关键区别 |
|---|---|---|
run() |
封装被线程执行的代码(重写它来设置线程要执行的操作) | 直接调用 run() 不会创建新线程,和普通方法调用一样(还是在当前线程执行) |
start() |
启动线程的方法 | 调用 start() 会启动一个新线程,然后由 JVM 在新线程中调用该线程的 run() |
要点:
- 为什么重写 run():
run()用来封装"线程启动后要执行的代码",所以自定义线程类的核心就是重写run()- 启动线程必须用 start():
start()启动新线程后由 JVM 自动调用 run();直接调run()只是普通方法调用,不产生新线程- 构造器传名:
MyThread调用super(name)把线程名传给Thread类,之后用this.getName()获取当前线程名,便于区分多个线程
MyThread 继承 Thread 类,重写 run() 方法,在 run() 中封装"求和 + 打印"的操作:
// 来源:course/MyThread.java(本次更新新增文件)
public class MyThread extends Thread {
public MyThread(String name) {
super(name); // 把线程名传给父类 Thread
}
/**
* 重写run方法,这个方法里面的内容,就是线程启动后具体执行的操作。
*/
@Override
public void run() {
int sum = 0;
for (int i = 0; i < 10000; i++) {
System.out.println(this.getName() + "--->" + i);
sum += i;
}
System.out.println(this.getName() + "--->" + sum);
System.out.println(this.getName() + "--->" + "hello world");
System.out.println(this.getName() + "--->" + "java....Thread....我的自定义线程....");
}
}
ThreadTest01(原 ThreadTest,本次已重命名)创建两个自定义线程对象(线程1、线程2),调用 start() 启动:
// 来源:course/ThreadTest01.java(本次更新由 ThreadTest 重命名而来)
public class ThreadTest01 {
public static void main(String[] args) {
// 创建自定义线程类的对象
MyThread mt1 = new MyThread("线程1");
MyThread mt2 = new MyThread("线程2");
// 调用自定义线程类的start()方法 启动线程。
mt1.start();
mt2.start();
}
}
要点:
- 启动两个线程:
mt1.start()/mt2.start()分别启动"线程1"、"线程2"两条执行路径,两条路径的run()交替并发执行(不是等线程1跑完再跑线程2)- getName() 区分线程:两个线程对象名不同,
this.getName()打印出 "线程1" / "线程2",可以直观看到两个线程的输出交错出现- 可反复运行观察:每次运行两个线程的执行顺序可能不同(由 CPU 调度决定),这正是"多线程并发"的特征
代码更新后,Demo04 补充了方式二:实现 Runnable 接口——相比方式一(继承 Thread),方式二更推荐:
// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
方式二:实现Runnable接口 更推荐这种
Thread类的构造方法:
public Thread(Runnable task) 创建一个线程类对象,传入Runnable接口的实现类对象。
public Thread(Runnable task, String name)创建一个线程类对象,传入Runnable接口的实现类对象。并且指定线程的名称
Runnable接口中有一个run()抽象方法,必须要实现类重写。
实现步骤:
1、定义一个实现类实现Runnable接口
2、在实现类中重写run()方法,指定线程的任务。
3、创建实现类对象
4、创建Thread类的对象,把实现类对象作为参数传入Thread的构造方法
5、调用Thread类的start()方法启动线程
方式二相比于方式一,更推荐,因为可以实现多个接口,并且可以继承其他类,不会改变程序类的上下文语义。
*/
// 来源:course/Demo04.java(注释部分)
public Thread(Runnable task) // 创建线程对象,传入 Runnable 接口实现类对象
public Thread(Runnable task, String name)// 创建线程对象,传入 Runnable 实现类对象,并指定线程名称
| 对比维度 | 方式一:继承 Thread | 方式二:实现 Runnable |
|---|---|---|
| 继承关系 | 必须继承 Thread,无法再继承其他类(Java 单继承) |
实现 Runnable 接口,不影响继承其他类 |
| 接口数量 | 已占用唯一的继承机会 | 还可以实现多个接口 |
| 程序语义 | 类与线程强耦合,类"本身就是一个线程" | 类与线程解耦——类是"任务",线程由 Thread 承载 |
| 推荐程度 | 基础入门 | 更推荐:灵活、符合"组合优于继承"思想 |
要点:
- Runnable 接口只有一个 run() 抽象方法,实现类必须重写它来指定"线程要执行的任务"
- Thread 构造方法接收 Runnable:
new Thread(runnableImpl, "线程名")—— 把"任务对象"传入线程,由 Thread 负责启动- 推荐原因:方式二实现了"任务"与"线程"的解耦——类只负责定义任务(实现 Runnable),至于开几个线程、用什么线程都由外部 Thread 决定;同时避免了 Java 单继承的限制
RunnableImpl 实现 Runnable 接口,在 run() 中打印"熊大熊二光头强"并循环输出当前线程名:
// 来源:course/RunnableImpl.java(本次更新新增文件)
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("熊大熊二光头强");
// 获取当前线程的名字
String name = Thread.currentThread().getName();
for (int i = 0; i < 1000; i++) {
System.out.println("我是" + name + "--->" + i);
}
}
}
ThreadTest02 演示了三种把任务交给线程的方式:实现类对象、匿名内部类、Lambda 表达式;启动三个子线程后,main 主线程也参与循环打印(本次更新新增),直观看到 4 条执行路径并发:
// 来源:course/ThreadTest02.java(本次更新新增,并补充主线程演示)
public class ThreadTest02 {
public static void main(String[] args) {
// 方式一:创建实现类对象,传入 Thread 构造方法
RunnableImpl ri = new RunnableImpl();
Thread t1 = new Thread(ri, "光头强线程!");
// 方式二:匿名内部类(Runnable 只有一个抽象方法 run(),可用匿名内部类)
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 1000; i++) {
System.out.println("我是" + name + "--->" + i);
}
}
}, "熊大线程!");
// 方式三:Lambda 表达式(Runnable 是函数式接口,可用 Lambda 简化匿名内部类)
Thread t3 = new Thread(() -> {
String name = Thread.currentThread().getName();
for (int i = 0; i < 1000; i++) {
System.out.println("我是" + name + "--->" + i);
}
}, "熊二线程!");
t1.start();// 执行了这个方法,才会创建一个新的线程
t2.start();
t3.start();
// 主线程 main 也参与循环打印(本次更新新增)——主线程与三个子线程并发执行
String name = Thread.currentThread().getName();
for (int i = 0; i < 10000; i++) {
System.out.println("我是" + name + "--->" + i);
}
}
}
要点:
- 三种提供 Runnable 的方式:①实现类对象
new Thread(ri, "光头强线程!");②匿名内部类new Thread(new Runnable(){...}, ...);③Lambda 表达式new Thread(() -> {...}, ...)——第三种最简洁,因为 Runnable 是函数式接口(只有一个抽象方法)Thread.currentThread().getName():方式二不继承 Thread,不能直接this.getName()(this 是 Runnable 实现类不是线程),所以用静态方法Thread.currentThread()拿到当前正在执行的线程对象,再调getName()获取线程名- 构造方法指定线程名:
new Thread(task, name)的第二个参数就是线程名,启动后Thread.currentThread().getName()会返回它(如 "光头强线程!")- 三个子线程并发执行:t1 / t2 / t3 的
start()启动三条执行路径,输出会交错出现- 主线程也参与:
start()之后 main 方法继续往下执行(不阻塞),Thread.currentThread().getName()返回main——主线程与三个子线程共 4 条执行路径并发,控制台输出交错混排(本次更新新增)
前两种方式(继承 Thread / 实现 Runnable)的 run() 方法没有返回值。如果需要线程计算完成后返回结果,就用方式三:实现 Callable 接口——它的 call() 方法带返回值,配合 FutureTask 使用:
// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
方式三:实现Callable接口,重写带有返回值的线程操作方法
call() 计算结果,如果无法计算结果,就会抛出一个异常。
FutureTask(Callable<V> callable) 创建一个FutureTask对象,一旦运行就执行参数中的callable对象
V get() 如果有必要,等待计算完成,然后获取计算的结果。
实现步骤:
1、自定义一个类实现Callable接口,并且可以指定泛型,这个泛型其实就是返回值的类型
2、自定义实现类中重写Callable接口的call()方法
3、创建自定义实现类的对象
4、创建FutureTask对象,并且把自定义实现类对象作为参数传入构造方法。
5、创建Thread类的对象,把FutureTask对象作为参数传入构造方法
6、调用Thread类的对象的start()方法,启动线程
7、可以调用get方法,获取线程结束后的结果。
*/
| 成员 | 说明 |
|---|---|
Callable<V> 接口 |
泛型接口,V 是 call() 的返回值类型;实现类必须重写 call() |
V call() |
带返回值的线程任务方法;无法计算结果时抛异常 |
FutureTask<V>(Callable<V> callable) |
包装 Callable 的任务对象,一旦运行就执行其中的 callable |
V get() |
等待计算完成,然后获取计算结果 |
要点:
- Callable vs Runnable 的本质区别:
Runnable.run()无返回值,Callable.call()有返回值(泛型指定返回类型),且call()声明可抛异常- FutureTask 是"中间人":Callable 不能直接交给 Thread(Thread 构造只收 Runnable/FutureTask),所以用
FutureTask包装 Callable,再把 FutureTask 传给 Thread- get() 阻塞等待:
get()会等待该线程计算完成再返回结果——它拿到的就是对应线程call()的返回值
CallableImpl 实现 Callable<Integer>(泛型指定返回类型为 Integer),重写 call() 计算 1~100000 的和并返回:
// 来源:course/CallableImpl.java(本次更新新增文件)
public class CallableImpl implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100000; i++) {
sum += i;
}
String name = Thread.currentThread().getName();
System.out.println(name + "---任务中-->" + sum);
return sum; // 返回值就是线程的计算结果
}
}
ThreadTest03 用同一个 CallableImpl 创建 3 个 FutureTask,启动 3 个线程,最后用 get() 拿到每个线程的计算结果:
// 来源:course/ThreadTest03.java(本次更新新增文件)
public class ThreadTest03 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1、创建自定义实现类的对象
CallableImpl ci = new CallableImpl();
// 2、创建FutureTask对象,把实现类对象作为参数传入构造方法
FutureTask<Integer> ft1 = new FutureTask<>(ci);
FutureTask<Integer> ft2 = new FutureTask<>(ci);
FutureTask<Integer> ft3 = new FutureTask<>(ci);
// 3、创建Thread对象,把FutureTask对象作为参数传入构造方法
Thread t1 = new Thread(ft1, "线程1");
Thread t2 = new Thread(ft2, "线程2");
Thread t3 = new Thread(ft3, "线程3");
// 4、start() 启动线程
t2.start();
t3.start();
t1.start();
// 5、调用get()获取线程结束后的结果
Thread.sleep(100); // 主线程休眠,让子线程先跑
Integer i1 = ft1.get();
System.out.println(t1.getName() + "---->" + i1);
Integer i2 = ft2.get();
System.out.println(t2.getName() + "---->" + i2);
Integer i3 = ft3.get();
System.out.println(t3.getName() + "---->" + i3);
}
}
要点:
- 同一个 Callable 三个线程:
ci被包装进 3 个 FutureTask(ft1/ft2/ft3),分别给 3 个线程——每个线程独立计算一份求和(FutureTask是任务容器,不是线程)- get() 获取返回值:
ft1.get()返回线程1 中call()的sum值,打印为 "线程1---->4999950000" 之类的结果- Thread.sleep(100):主线程休眠 100 毫秒,给子线程留出执行时间,保证
get()之前任务大体已跑完(get()本身也会等待)- 两种异常:main 声明
throws ExecutionException, InterruptedException——get()抛 ExecutionException(任务内部异常),sleep()抛 InterruptedException
// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
三种实现方式的对比:
实现Runnable、Callable接口
好处:扩展性更强,实现这个接口的同时也可以实现其他接口以及继承其他父类。
缺点:编程相对更复杂,不能直接使用Thread类中的方法
继承Thread类
好处:编程比较简单,可以直接使用Thread类中的方法
缺点:可扩展性比较差,不能再继承其他类。
*/
| 维度 | 继承 Thread | 实现 Runnable | 实现 Callable |
|---|---|---|---|
| 任务返回结果 | run() 无返回值 |
run() 无返回值 |
call() 有返回值 |
| 扩展性 | 差(单继承占位,不能再继承其他类) | 强(可多实现接口 + 可继承父类) | 强(同 Runnable) |
| 编程难度 | 简单 | 较复杂 | 较复杂(需 FutureTask 包装) |
| 直接使用 Thread 方法 | 可以(this 就是线程) | 不可以(用 Thread.currentThread()) |
不可以 |
| 推荐程度 | 入门 | 更推荐 | 需要返回结果时用 |
要点:
- 接口派优势:实现 Runnable / Callable 接口扩展性更强——实现这个接口的同时还可以实现其他接口、继承其他父类;缺点是不能直接用 Thread 类的方法(需
Thread.currentThread())- 继承派优势:继承 Thread 类编程更简单、可以直接使用 Thread 类的方法;缺点是可扩展性差,不能再继承其他类(Java 单继承)
- 选型建议:默认用实现接口(Runnable);需要线程返回计算结果时用 Callable + FutureTask
// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
Thread类的其他常用的方法:
setName(String name) 为这个线程设置线程名
String getName() 获取这个线程的线程名
Thread currentThread() 返回当前正在执行的线程对象的引用
*/
要点:
setName(String)/getName():实例方法,设置 / 获取当前线程对象的线程名(等价于构造方法传 name 的方式)Thread.currentThread():静态方法,返回当前正在执行这段代码的线程对象的引用——在实现 Runnable / Callable 接口时(this 不是线程),用它拿线程对象再调getName()等实例方法
本课知识点围绕"字符集转码 + 对象持久化 + 多线程入门"展开,一张表看清"知识 → 代码位置":
| 知识点 | 具体体现 | 源码位置 |
|---|---|---|
| 编码 / 解码模型 | 字符--编码-->字节--解码-->字符;A 编码 + B 解码 = 乱码 | course/Demo01 注释 |
| 转换流概念 | 把 A 字符集的文件转换为 B 字符集的文件(如 GBK 追加内容 → 先转 UTF-8) | course/Demo01 注释 |
| InputStreamReader | 字节流→字符流桥梁(父类 Reader);读取字节按指定字符集解码为字符 | course/Demo01 |
| OutputStreamWriter | 字符流→字节流桥梁(父类 Writer);读取字符按指定字符集编码为字节 | course/Demo01 |
| 转换流构造方法 | InputStreamReader(InputStream) / (InputStream, String charsetName);OutputStreamWriter 同理,可指定字符集名称 |
course/Demo01 注释 |
| 转码综合案例 | UTF-8 读 abc.txt → GBK 写 hello.txt:new InputStreamReader(new FileInputStream(...), "UTF-8") + new OutputStreamWriter(new FileOutputStream(...), "GBK") + char[1024] 循环 |
course/Demo01 main |
| 对象持久化概念 | 把内存中对象保存到磁盘 / 网络中传输;字节序列含对象类型、数据、属性 | course/Demo02 注释 |
| 对象序列化流 ObjectOutputStream | public ObjectOutputStream(OutputStream out) + writeObject(Object) 写入对象 |
course/Demo02、ObjectOutputStreamTest |
| 对象反序列化流 ObjectInputStream | public ObjectInputStream(InputStream in) + readObject() 返回 Object(需强转) |
course/Demo02、ObjectInputStreamTest |
| Serializable 标识接口 | 无方法无属性,仅作"可被序列化"的标识;类必须实现才能序列化 | course/Demo02 注释、Student |
| serialVersionUID 版本兼容 | private static final long serialVersionUID = 42L; 固定版本号;类属性变化后反序列化不报 InvalidClassException,缺的属性赋默认值 |
course/Demo02 注释、Student |
| serialVersionUID 三大作用 | ①验证反序列化版本一致性 ②未声明时 JVM 按类结构自动生成 ③类结构变→自动版本号变→InvalidClassException;最佳实践显式声明(值无所谓,两端一致即可) | course/Demo02 注释 |
| InvalidClassException | 类结构变化后旧序列化数据反序列化直接报错 | course/Demo02 注释 |
| transient 跳过序列化 | private transient Integer id; 该属性不参与序列化,反序列化为默认值(null) |
course/Demo02 注释、Student |
| 序列化注意事项(6 条) | 静态属性不序列化;transient 不序列化;父类 Serializable 子类自动序列化;反序列化不调用构造方法;引用链所有对象必须可序列化;单例需重写 readResolve() | course/Demo02 注释 |
| 静态属性不参与序列化 | static 成员属于类、不属于对象,天然不参与序列化 | course/Demo02 注释 |
| 父类 Serializable → 子类自动序列化 | 继承体系中父类实现 Serializable,子类无需重复实现 | course/Demo02 注释 |
| 反序列化不调用构造方法 | readObject() 按字节序列在内存中重构对象,不走构造方法 / 初始化块 | course/Demo02 注释 |
| 引用链必须全部可序列化 | 对象引用其他对象时,引用链上所有对象必须实现 Serializable,否则抛 NotSerializableException | course/Demo02 注释 |
| 单例 readResolve() | 反序列化绕过构造器会破坏单例;重写 readResolve() 返回既有单例保持全局唯一 | course/Demo02 注释 |
| Student 实体类 | implements Serializable + serialVersionUID + transient + equals/hashCode/toString 重写(Objects 工具类) | course/Student |
| 序列化写端演示 | 创建对象 → ObjectOutputStream 包装文件流 → writeObject(s) → close | course/ObjectOutputStreamTest |
| 反序列化读端演示 | ObjectInputStream 包装文件流 → readObject() → println 输出(调用 toString) | course/ObjectInputStreamTest |
| 课后作业:接口化复制 | CopyFile 接口 + Copy01(普通字节流 try-catch-finally)+ BufferedCopy(缓冲流 throws)+ currentTimeMillis 耗时对比 | homework0807/Demo_03 |
| 扩展流(处理流)体系 | 缓冲流(08-07)+ 转换流 + 对象流均属于"处理流":包装底层字节流,增加缓冲 / 转码 / 序列化能力(Demo03 完整汇总) | course/Demo01、Demo02、Demo03 |
| IO 流体系总结(Demo03) | 基础流(字节流 InputStream/OutputStream、字符流 Reader/Writer)+ 处理流(缓冲/转换/对象流);常用 write/read/flush/close;特殊 readLine/newLine/writeObject/readObject;特殊属性 serialVersionUID;特殊关键字 transient | course/Demo03 |
| 多线程概念 | 程序是硬盘中的文件;内存中执行的程序是进程;进程中的单个顺序控制流是线程;main 方法是主线程 | course/Demo04、Hello |
| 进程三大特性 | 独立性(独立运行 / 资源分配调度单位)、动态性(一次执行过程,动态产生消亡)、并发性(可与其他进程并发执行) | course/Demo04 注释 |
| 并行 vs 并发 | 并行:同一时刻多指令在多个 CPU 上同时执行;并发:同一时刻多指令在单个 CPU 上交替执行(后厨灶台类比) | course/Demo04 注释 |
| CPU vs GPU | CPU 适合复杂运算;GPU 适合简单重复量大的并行运算(挖矿 / 3A 游戏 / AI 大模型) | course/Demo04 注释 |
| 继承 Thread 实现多线程 | ①自定义类继承 Thread ②重写 run() 封装线程操作 ③创建线程对象 ④调 start() 启动 | course/Demo04 注释、MyThread |
| run() vs start() | run() 封装被线程执行的代码,直接调用不创建新线程(普通方法);start() 启动新线程,由 JVM 调用该线程的 run() | course/Demo04 注释 |
| 构造器传线程名 | MyThread(String name) { super(name); } 把线程名传给 Thread,this.getName() 获取线程名 |
course/MyThread |
| 启动多线程演示(方式一) | ThreadTest01(原 ThreadTest)创建线程1 / 线程2 两个线程对象,分别 start(),run() 交替并发执行(输出交错) | course/ThreadTest01 |
| 实现 Runnable 接口(方式二) | ①实现类实现 Runnable ②重写 run() ③创建实现类对象 ④new Thread(实现类对象, 线程名) ⑤start() 启动;比方式一更推荐(不占继承位、可多实现接口、任务与线程解耦) | course/Demo04 注释、RunnableImpl |
| Thread 构造方法接收 Runnable | Thread(Runnable task) / Thread(Runnable task, String name) —— 传入任务对象并可指定线程名 |
course/Demo04 注释 |
| Thread.currentThread().getName() | 方式二不继承 Thread,用静态方法 Thread.currentThread() 获取当前线程对象再取线程名 | course/RunnableImpl、ThreadTest02 |
| 匿名内部类提供 Runnable | new Thread(new Runnable(){ ... }, "熊大线程!") —— Runnable 可用匿名内部类实现 |
course/ThreadTest02 |
| Lambda 提供 Runnable | new Thread(() -> { ... }, "熊二线程!") —— Runnable 是函数式接口,Lambda 最简洁 |
course/ThreadTest02 |
| 三种提供任务方式对比 | 实现类对象(最传统)/ 匿名内部类(临时任务)/ Lambda(函数式接口最简洁) | course/ThreadTest02 |
| 实现 Callable 接口(方式三) | ①类实现 Callable<V> 指定返回类型 ②重写 call() ③创建对象 ④new FutureTask<V>(callable) ⑤new Thread(futureTask) ⑥start() ⑦get() 获取结果;call() 带返回值、可抛异常 | course/Demo04 注释、CallableImpl |
| FutureTask 包装 Callable | FutureTask<V>(Callable<V> callable) 包装 Callable 任务,传给 Thread 构造;V get() 等待计算完成获取结果 |
course/ThreadTest03 |
| get() 获取线程结果 | ft.get() 阻塞等待该线程 call() 计算完成,返回结果;抛 ExecutionException / InterruptedException |
course/ThreadTest03 |
| 三种实现方式对比 | 继承 Thread:简单但扩展性差(占单继承位);实现 Runnable/Callable:扩展性强(可多实现接口 + 可继承父类)但编程复杂、不能直接用 Thread 方法 | course/Demo04 注释 |
| Thread 常用方法 | setName(String) 设线程名 / getName() 取线程名 / 静态 Thread.currentThread() 返回当前执行线程对象 |
course/Demo04 注释、RunnableImpl、CallableImpl |
| 主线程参与并发 | ThreadTest02 中 start() 后 main 继续执行循环打印,主线程与 3 个子线程共 4 条路径并发(输出交错) | course/ThreadTest02 |
Demo01 main,把 D:/abc.txt(UTF-8)转码为 GBK 的 D:/hello.txt,再用 InputStreamReader(...,"GBK") 读回验证内容正确;改错字符集(如读端写 UTF-8)观察乱码现象InputStreamReader + OutputStreamWriter 完成"读 GBK 文件 → 写 UTF-8 文件",体会两个构造参数 String charsetName 的作用FileReader/FileWriter 只能使用默认字符集;改用 InputStreamReader/OutputStreamWriter 手动指定字符集,对比异同,理解"FileReader = 字节流 + 默认字符集"ObjectOutputStreamTest 写端(取消注释),把 Student 对象序列化到 D:/studentObject.txt,再运行 ObjectInputStreamTest 反序列化读回,观察控制台输出Student 的 id 输出是否为 null(因为 id 被 transient 修饰),验证"transient 属性不参与序列化"InvalidClassException,再加上 serialVersionUID 对比验证兼容效果ObjectInputStreamTest 中 Object o = ois.readObject() 改为 Student s = (Student) ois.readObject();,强转后调用 s.getName() 等 getter 方法writeObject() 序列化多个 Student 对象,再连续 readObject() 读回,体会字节序列的顺序性static 字段,序列化 / 反序列化后验证其值不受影响(静态属性不参与序列化);②让 Student 引用一个未实现 Serializable 的类(如 Address),序列化观察 NotSerializableException 指向引用链上哪个类;③在构造方法中加打印语句,反序列化时观察是否触发(验证"反序列化不调用构造方法")readResolve() 返回既有单例后再次验证Demo03 小结,自己画出完整 IO 流体系图(基础流:字节 / 字符流;处理流:缓冲流 / 转换流 / 对象流),并说出每个流"处理什么、怎么配对包装、有什么特有方法"homework0807/Demo_03,用同一个大文件对比 Copy01(普通字节流)与 BufferedCopy(缓冲流)的复制耗时,验证缓冲流效率更高;再把 Copy01 的 try-catch-finally 与 BufferedCopy 的 throws 两种异常处理风格对比MyThread 自定义一个线程类(继承 Thread + 重写 run()),在 run() 中循环打印 0~999;创建两个线程对象并 start() 启动,观察两个线程的输出交替出现,体会并发执行mt.run() 和 mt.start() 启动同一个线程,观察输出区别——直接调 run() 时只有一个线程顺序执行(先执行完 run 再走下一步),而 start() 启动的是独立线程main 中先启动两个子线程再打印 "main 主线程" 内容,观察"主线程输出"与"子线程输出"的先后顺序(由 CPU 调度决定,顺序不定)RunnableImpl 定义一个实现类(implements Runnable + 重写 run()),创建两个 Thread 对象并指定不同线程名,start() 启动,观察两个线程输出交错;用 Thread.currentThread().getName() 打印当前线程名ThreadTest02 中分别用实现类对象、匿名内部类、Lambda 表达式创建三个线程(光头强 / 熊大 / 熊二),体会三种写法从繁琐到简洁的演进,重点理解 Lambda 为什么能替代匿名内部类(Runnable 是函数式接口)new Thread(task, "自定义名") 指定线程名后,在 run() 中用 Thread.currentThread().getName() 打印,验证拿到的是构造时指定的名字;不指定名字时观察默认线程名(如 Thread-0 / Thread-1)MyThread(继承 Thread 方式)改造成"实现 Runnable 方式"的 MyRunnable,再在 ThreadTest02 中用实现类对象方式启动,体会两种方式代码结构的不同CallableImpl 定义一个实现 Callable<Integer> 的类,重写 call() 计算 1~N 的和并返回;用 FutureTask 包装后启动线程,get() 打印返回值call() 中加 Thread.sleep() 模拟耗时计算,观察 get() 调用前主线程是否被阻塞(get 会等待计算完成);比较"调用 get() 前打印"和"调用 get() 后打印"的输出顺序ThreadTest02,观察控制台是否同时出现 "main"、"光头强线程!"、"熊大线程!"、"熊二线程!" 四类输出交错混排,验证主线程与子线程并发执行setName("新名字") 动态修改线程名,再用 getName() 打印验证;在 main 中调用 Thread.currentThread().getName() 确认主线程名是 "main"java.io.InputStreamReader、java.io.OutputStreamWriter、java.io.ObjectOutputStream、java.io.ObjectInputStream、java.io.SerializableFileReader / FileWriter 只能使用默认字符集,而 InputStreamReader / OutputStreamWriter 可以手动指定字符集,是处理跨字符集文件的利器;常用的 UTF-8 是 Unicode 的一种可变长实现,兼容 ASCIIObjectOutputStream(BufferedOutputStream(FileOutputStream(...)))),每层各加一种能力(缓冲 / 转码 / 序列化)Serializable、Cloneable 等接口没有任何方法,仅作为"能力标识";instanceof Serializable 判断对象是否可序列化Serializable 的类显式声明 private static final long serialVersionUID = 1L;,否则类一旦修改结构,JVM 自动生成版本号变化会导致反序列化抛 InvalidClassExceptionstatic 修饰的成员属于类、不属于对象,本身就不参与序列化;transient 用于让实例成员也跳过序列化Serializable 则子类自动可序列化;若父类未实现 Serializable,则父类部分字段不参与序列化,且反序列化时会调用父类的无参构造器(对比:子类部分反序列化不调用构造方法)Serializable 就会抛此异常,异常信息会点名是哪个类——排查时顺着引用链逐级检查readResolve() 是单例模式对抗序列化的标准手段——在 readResolve() 中返回内存中既有的单例实例,保证全局唯一Serializable 外还有 Externalizable 接口,实现 writeExternal() / readExternal() 可完全自定义序列化内容(比 Serializable 更可控但需手动实现)java.lang.Thread(线程类,run() / start() / getName() 等核心方法)Runnable 是函数式接口(只有一个抽象方法 run()),所以可以用 Lambda 表达式 new Thread(() -> {...}) 或方法引用简化匿名内部类——这是 08-04 学过的函数式编程在并发编程中的应用this.getName();Thread.currentThread() 是静态方法,返回当前正在执行该行代码的线程对象,配合 getName() / setName() / getPriority() 使用synchronized 等机制解决java.util.concurrent.Callable、java.util.concurrent.FutureTask、java.util.concurrent.FutureFutureTask 实现了 Future 接口——get() 获取结果、isDone() 判断任务是否完成、cancel() 取消任务;Future 就是"异步计算的结果占位符",体现了异步编程思想ExecutorService 线程池的 submit(Callable) 方法直接返回 Future 对象,是生产环境"提交带返回值的任务"的主流写法(比手动 new Thread + FutureTask 更规范)start() 只是让线程进入就绪,具体运行由 CPU 调度synchronized 同步代码块 / 同步方法、Lock 锁、volatile 等(后续课程学习)sleep(毫秒)(让线程休眠)、join()(等待线程执行完)、yield()(让出 CPU)、setName/getName(设置/获取线程名)、isAlive()(判断线程是否存活)等java.util.concurrent 并发包(ThreadPoolExecutor、ConcurrentHashMap、CountDownLatch 等),是生产环境多线程编程的主流工具