20260808-笔记.md 81 KB

20260808 课堂笔记 — 转换流(InputStreamReader / OutputStreamWriter 字节↔字符桥梁 + 字符集编码解码与乱码成因 + UTF-8 转 GBK 文件转码案例)+ 对象持久化与序列化(对象持久化概念 + Serializable 标识接口 + ObjectOutputStream 序列化 writeObject / ObjectInputStream 反序列化 readObject + serialVersionUID 三大作用与版本兼容 + transient 跳过序列化 + 序列化注意事项(静态属性 / 引用链 / 构造方法 / 单例 readResolve)+ Student 实体类综合演示 + IO 流体系总结(基础流 vs 处理流全景))+ 多线程入门(进程 / 线程 / 并行并发概念 + 方式一继承 Thread / 方式二实现 Runnable / 方式三实现 Callable+FutureTask + 三种实现方式对比 + run() 与 start() 区别 + 匿名内部类与 Lambda 创建 Runnable + main 主线程)+ 课后作业回顾(接口化文件复制效率对比器)

  • 日期:2026-08-08
  • 项目:c260808
  • 包路径:course / homework0807
  • 作者:WanJL

目录

  1. 转换流概念:字符集编码 / 解码与乱码成因(Demo01)
  2. InputStreamReader —— 字节流到字符流的桥梁
  3. OutputStreamWriter —— 字符流到字节流的桥梁
  4. 转换流综合案例:UTF-8 文件转 GBK 文件(Demo01 main)
  5. 对象持久化概念:把内存中的对象保存到磁盘(Demo02)
  6. 对象序列化:ObjectOutputStream + writeObject()
  7. 对象反序列化:ObjectInputStream + readObject()
  8. Serializable 标识接口与 serialVersionUID 版本兼容(InvalidClassException)
  9. transient 关键字:跳过序列化的属性
  10. 序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新)
  11. IO 流体系总结:基础流 vs 处理流全景(Demo03)
  12. Student 实体类 —— 可序列化对象综合演示
  13. 序列化 / 反序列化运行演示:ObjectOutputStreamTest / ObjectInputStreamTest
  14. 课后作业回顾:接口化文件复制效率对比器(homework0807/Demo_03)
  15. 多线程入门:进程、线程、并行与并发、main 主线程(Demo04 与 Hello)
  16. 实现多线程方式一:继承 Thread 类,重写 run()(MyThread 与 ThreadTest01)
  17. 实现多线程方式二:实现 Runnable 接口(RunnableImpl 与 ThreadTest02)
  18. 实现多线程方式三:实现 Callable 接口 + FutureTask(CallableImpl 与 ThreadTest03)
  19. 知识点全景总结
  20. 随堂练习要点
  21. 拓展阅读

1. 转换流概念:字符集编码 / 解码与乱码成因(Demo01)

概念 —— 为什么要转换流

上一日学习的字符流使用平台默认字符集读写文件。但现实中会遇到这种情况:一个文件本身是 GBK 编码的,而我们手头只能使用 UTF-8 字符集,想要给这个文件追加写入内容——直接写必然乱码。这时就需要转换流:把 A 字符集的文件,转换为 B 字符集的文件。

// 来源:course/Demo01.java(注释部分)
/*
    我们讲字符流的时候,说过字符集的概念,
    不同的字符集虽然都能转换成字节(二进制数)但是,同一个字节,在不同的字符集中表示的含义是不一样的。

    字符--编码-->字节--解码-->字符
    如果使用字符集A进行编码,使用字符集B进行解码,就会出现字符集乱码。
*/

编码 / 解码模型

字符 --编码--> 字节 --解码--> 字符

要点:

  • 编码(Encoding):字符 → 字节(字符集编码表负责把文字符号转成二进制数)
  • 解码(Decoding):字节 → 字符(把二进制数按编码表还原成文字符号)
  • 乱码根源:不同字符集虽然都能把字符转成字节,但同一个字节在不同字符集中表示的含义不一样——用字符集 A 编码、用字符集 B 解码,就会得到错误字符(乱码)
  • 转换流的应用场景:把 GBK 编码的文件内容转成 UTF-8(或反之),实现不同字符集文件之间的转换

2. InputStreamReader —— 字节流到字符流的桥梁

概念 —— 读取字节并用指定字符集解码为字符

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")则按指定字符集解码

3. OutputStreamWriter —— 字符流到字节流的桥梁

概念 —— 读取字符并用指定字符集编码为字节

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)本质上就是"字节流 + 默认字符集",而转换流允许手动指定字符集,更加灵活

4. 转换流综合案例:UTF-8 文件转 GBK 文件(Demo01 main)

案例目标

使用 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 处理(教学简化写法)

5. 对象持久化概念:把内存中的对象保存到磁盘(Demo02)

概念 —— 为什么要对象持久化

类文件(.java / .class)可以分享给其他人,但根据类创建的对象是在程序运行时于内存中开辟空间创建的,无法直接分享。通过对象持久化,可以把内存中的对象保存到磁盘,或者在网络中传输对象,从而分享给他人。

// 来源:course/Demo02.java(注释部分)
/*
    我们可以把一个类文件分享给其他人,但是如果是根据某个类创建的对象,想要分享给其他人,就不容易了
    因为它是程序运行时,在内存中开辟空间创建的对象。
    但是我们可以通过【对象持久化】的方式,把内存中的对象,保存到磁盘中,分享其他人。
    对象持久化:就是把对象保存到磁盘、或者在网络中传输对象。
    这种机制就是使用一个字节序列表示一个对象,这个字节序列包含:
        对象的类型
        对象的数据,和对象中存储的属性等等信息。
    把字节序列写入到文件中后,就相当于永久的保存了一个对象。
    反过来,可以使用【对象反序列化】的方式再从文件中读取出对象,进行对象重构。
*/

序列化 / 反序列化模型

序列化(保存):对象(内存) --对象序列化流 ObjectOutputStream--> 字节序列 --> 磁盘文件(永久保存)
反序列化(读取):磁盘文件 --对象反序列化流 ObjectInputStream--> 字节序列 --> 内存中重新构造对象

要点:

  • 对象持久化:把内存中的对象保存到磁盘、或在网络中传输对象——本质是把"对象"变成字节序列
  • 字节序列的内容:包含对象的类型、对象的数据、对象中存储的属性等信息,所以反序列化时能完整还原对象
  • 序列化:对象 → 字节序列 → 文件,永久保存对象
  • 反序列化:从文件读取字节序列 → 在内存中重构对象
  • 类比:序列化像"拍快照存档",反序列化像"读档恢复"

6. 对象序列化:ObjectOutputStream + writeObject()

概念 —— 对象序列化流

进行对象序列化的方式是使用对象序列化流 ObjectOutputStream,它把对象写入到字节输出流中:

// 来源:course/Demo02.java(注释部分)
/*
    进行对象序列化的方式就是使用【对象序列化流】    ObjectOutputStream
    构造方法:
    public ObjectOutputStream(OutputStream out) 创建一个写入指定OutputStream的对象序列化流对象

    对象序列化的方法:
        public final void writeObject(Object obj)   将指定对象obj写入到ObjectOutputStream
*/

前提 —— 必须实现 Serializable 接口

// 来源:course/Demo02.java(注释部分)
/*
    一个对象想要被序列化,必须要实现一个接口 java.io.Serializable
    Serializable接口没有任何需要重写的方法,也没有任何属性,它就像一个标识,标志着这个类的对象可以被序列化。
*/

要点:

  • 构造方法:new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt")) —— 同样先用文件字节输出流,再包上对象流
  • 核心方法:writeObject(Object obj) 把任意对象写入流
  • Serializable 是标记接口(标识接口):没有任何抽象方法、没有任何属性,它像一个"通行证标识",标志该类的对象可以被序列化——凡是支持序列化的类都必须实现它

7. 对象反序列化:ObjectInputStream + readObject()

概念 —— 对象反序列化流

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

8. Serializable 标识接口与 serialVersionUID 版本兼容(InvalidClassException)

经典场景 —— 类版本变化后反序列化报错

// 来源: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,避免类升级导致旧数据无法读取

serialVersionUID 的三大作用(Demo02 更新)

代码更新后,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),只要序列化 / 反序列化两端一致即可——这就是"显式声明版本号能免疫类结构变化"的根本原因

9. transient 关键字:跳过序列化的属性

概念 —— 不希望参与序列化的成员

如果类中某个属性不想要被序列化(例如敏感信息、临时数据),可以给该成员加 transient 关键字修饰,序列化时这个属性不会参与序列化:

// 来源:course/Demo02.java(注释部分)
/*
    如果类中有某个属性不行被序列化,可以给这个成员添加transient关键字修饰:
    private transient Integer id;
    那么在序列化的时候,这个属性。就不会参与序列化。
*/

要点:

  • transient 修饰成员变量:private transient Integer id; —— 序列化时跳过该属性
  • 反序列化结果:被 transient 修饰的属性,反序列化后为默认值(引用类型为 null、基本类型为 0/false)
  • 典型用途:密码、缓存、与对象状态无关的临时数据等不宜持久化的字段

10. 序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新)

概念 —— 序列化的 6 条使用须知

代码更新后,Demo02 补充了序列化的注意事项,总结了使用对象序列化时必须注意的 6 条规则:

// 来源:course/Demo02.java(注释部分,本次更新新增)
/*
    序列化的注意事项:
        1、静态属性不能序列化,因为静态属性是属于类的,不是属于某个对象的。
        2、transient修饰的属性不能序列化
        3、父类实现了java.io.Serializable接口,那么子类可以自动序列化。
        4、反序列化不会调用构造方法
        5、如果对象引用了其他的对象,那么引用链上的所有的对象必须是可以序列化的。
        6、单例对象需要重写一个方法readResolve()方法
*/

6 条注意事项逐条解读

# 注意事项 解读
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() 是单例 + 序列化的经典组合:重写后反序列化返回的仍是内存中唯一的那个实例

11. IO 流体系总结:基础流 vs 处理流全景(Demo03)

概念 —— 新增 Demo03 对 IO 流体系的完整总结

Demo03 是本日(乃至整个 IO 流阶段)知识的总收口:把之前学过的所有 IO 流按"基础流 / 处理流"两大维度完整归类,并汇总了常用方法、特殊方法、特殊属性、特殊关键字:

// 来源:course/Demo03.java(本次更新新增文件)
/*
    基础流:
        字节流:
            字节输入流:InputStream
                |-文件字节输入流:FileInputStream
            字节输出流:OutputStream
                |-文件字节输出流:FileOutputStream
        字符流:
            字符输入流:Reader
                |-文件字符输入流:FileReader
            字符输出流:Writer
                |-文件字符输出流:FileWriter
    处理流:
        缓冲流:
            缓冲字节流:BufferedInputStream / BufferedOutputStream
            缓冲字符流:BufferedReader / BufferedWriter
        转换流:
            输入:InputStreamReader---把字节流根据指定的字符集转换为字符
            输出:OutputStreamWriter--把字符根据指定的字符集转化为字节流
        对象流:
            序列化:ObjectOutputStream
            反序列化:ObjectInputStream
*/

IO 流体系全景图(Demo03 汇总版)

                                        ┌─ 字节流(处理任意类型文件)
                                        │    ├─ 输入: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(跳过序列化)——对象流专属的"配置项"

12. Student 实体类 —— 可序列化对象综合演示

实现 —— 把今天所有知识点落到实体类上

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 类知识点复用)

13. 序列化 / 反序列化运行演示:ObjectOutputStreamTest / ObjectInputStreamTest

写端 —— ObjectOutputStreamTest 序列化(注释保留)

// 来源: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();
}

读端 —— ObjectInputStreamTest 反序列化

// 来源: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} 这样的可读格式

14. 课后作业回顾:接口化文件复制效率对比器(homework0807/Demo_03)

作业背景

homework0807/Demo_03.java 是 8 月 7 日课后作业第三题(接口化文件复制效率对比器),把 8 月 7 日学过的"接口化设计 + 字节缓冲流"做了一次综合应用,与本日的"扩展流(处理流)"体系呼应。

① 复制器接口:统一复制规范

// 来源:homework0807/Demo_03.java
interface CopyFile {
    String copy(String src, String desc) throws IOException;
}

② 普通字节流实现 Copy01:FileInputStream + FileOutputStream + byte[8192] + try-catch-finally

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

③ 缓冲流实现 BufferedCopy:BufferedOutputStream / BufferedInputStream 包装底层流

// 来源: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 "复制成功";
    }
}

④ main —— System.currentTimeMillis() 耗时对比

// 来源: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

15. 多线程入门:进程、线程、并行与并发、main 主线程(Demo04 与 Hello)

概念 —— 什么是进程、什么是线程

代码更新后,c260808 新增了多线程主题。首先理解三个基础概念:程序、进程、线程。

// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
    什么线程?什么叫进程?
        我们知道,程序本质上就是一种文件,它是存储在硬盘中,程序想要被执行,就需要被加载到内存中执行。
        在内存中执行的着的程序就是进程。
        一个程序运行起来后,就成了进程,需要占用CPU、内存、网络、GPU、磁盘等等....
        一个进程中可能包含多个线程。
        比如:360安全卫士,同时进行木马查杀、电脑清理、系统修复、优化加速,每使用一个功能,就是启动一个线程。
*/
概念 说明
程序 本质是存储在硬盘中的文件,是一段静态的代码
进程 在内存中正在执行的程序;占用 CPU、内存、网络、GPU、磁盘等资源
线程 进程中的单个顺序控制流,是一条执行路径;一个进程可能包含多个线程

要点:

  • 一个程序运行起来后就成了进程,需要占用各种硬件资源(CPU / 内存 / 网络 / GPU / 磁盘)
  • 一个进程中可能包含多个线程——例如 360 安全卫士同时进行木马查杀、电脑清理、系统修复、优化加速,每个功能就是一个线程
  • 进程三大特性:独立性(能独立运行、系统分配资源和调度的独立单位)、动态性(进程本质是程序的一次执行过程,动态产生、动态消亡)、并发性(任何进程都可以和其他进程一起并发执行)
  • 单线程 vs 多线程:一个进程只有一条路径就是单线程程序;有多条路径就是多线程程序
  • main 主线程:main 方法被称为主方法,也就是 Java 程序的主线程(Hello.java 中的 main 就是主线程的入口)

并行 vs 并发

// 来源:course/Demo04.java(注释部分)
/*
    线程的并行和并发
    并行:同一个时刻,有多个指令在多个CPU上同时执行。比如:饭店后厨,有3个灶台被三位厨师使用炒菜。
    并发:同一时刻,有多个指令在单个CPU上交替执行。比如:饭店后厨,有3个灶台,被一位厨师炒菜。
*/
概念 含义 后厨类比
并行(Parallel) 同一个时刻,多个指令在多个 CPU 上同时执行 3 个灶台被 3 位厨师同时使用炒菜
并发(Concurrent) 同一时刻,多个指令在单个 CPU 上交替执行 3 个灶台被 1 位厨师轮番炒菜

要点:

  • 并行靠硬件:需要多个 CPU 核心同时工作,是"真·同时"
  • 并发靠调度:单核 CPU 在极短时间内快速切换执行多个线程,宏观上"像同时在跑",微观上是交替执行
  • 多线程的意义:从软硬件上实现多个线程并发执行,充分利用 CPU,提升性能——这也解释了为什么"多线程 ≠ 一定快"(单核场景下是并发切换)

补充 —— CPU 与 GPU 的分工(Demo04 注解)

Demo04 还顺带对比了 CPU / GPU 的特性差异,帮助理解"多核 / 多线程"的硬件背景:

// 来源:course/Demo04.java(注释部分)
/*
    CPU 计算机核心
        红色警戒    没有GPU,全靠CPU
        特性 适合进行复杂运算,但是不适合进行简单的重复性大量运算
    GPU 显卡核心
        比特币挖矿   CPU不适合挖矿,比特币本质上就是一种计算机运算,
        运算难度不高,复杂程度也不高,但是量大。
        3A游戏,只狼、荒野大镖客、GTA5、MC
        AI  大模型--->GPU
        本地部署AI大模型,也是吃GPU
*/

要点:

  • CPU(计算机核心):适合复杂运算,但不适合简单、重复、量大的运算(如挖矿)
  • GPU(显卡核心):适合大量简单并行运算——比特币挖矿、3A 游戏渲染、本地部署 AI 大模型都吃 GPU
  • 这个对比呼应"多线程"的性能本质:不同特性的任务交给不同核心处理,才能发挥硬件的并发能力

16. 实现多线程方式一:继承 Thread 类,重写 run()(MyThread 与 ThreadTest01)

概念 —— Java 多线程的第一种实现方式

Demo04 给出了 Java 实现多线程的方式一:继承 Thread 类,重写 run() 方法,共 4 个步骤:

// 来源:course/Demo04.java(注释部分)
/*
    Java中实现多线程的方式:
        方式一: 继承 Thread 类,重写 run()方法。
            实现步骤:
                1、自定义一个类,继承Thread类
                2、在自定义类中重写父类的run()方法,设置在线程中要执行的操作。
                3、创建自定义线程类的对象
                4、调用自定义线程类的start()方法 启动线程。
*/

run() 方法 vs 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

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

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 调度决定),这正是"多线程并发"的特征

17. 实现多线程方式二:实现 Runnable 接口(RunnableImpl 与 ThreadTest02)

概念 —— Java 多线程的第二种实现方式(更推荐)

代码更新后,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()方法启动线程

   方式二相比于方式一,更推荐,因为可以实现多个接口,并且可以继承其他类,不会改变程序类的上下文语义。
*/

Thread 类的构造方法(接收 Runnable)

// 来源:course/Demo04.java(注释部分)
public Thread(Runnable task)             // 创建线程对象,传入 Runnable 接口实现类对象
public Thread(Runnable task, String name)// 创建线程对象,传入 Runnable 实现类对象,并指定线程名称

方式二 vs 方式一 —— 为什么更推荐

对比维度 方式一:继承 Thread 方式二:实现 Runnable
继承关系 必须继承 Thread,无法再继承其他类(Java 单继承) 实现 Runnable 接口,不影响继承其他类
接口数量 已占用唯一的继承机会 还可以实现多个接口
程序语义 类与线程强耦合,类"本身就是一个线程" 类与线程解耦——类是"任务",线程由 Thread 承载
推荐程度 基础入门 更推荐:灵活、符合"组合优于继承"思想

要点:

  • Runnable 接口只有一个 run() 抽象方法,实现类必须重写它来指定"线程要执行的任务"
  • Thread 构造方法接收 Runnable:new Thread(runnableImpl, "线程名") —— 把"任务对象"传入线程,由 Thread 负责启动
  • 推荐原因:方式二实现了"任务"与"线程"的解耦——类只负责定义任务(实现 Runnable),至于开几个线程、用什么线程都由外部 Thread 决定;同时避免了 Java 单继承的限制

实现类 —— RunnableImpl

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(三种提供 Runnable 的方式 + 主线程演示)

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 条执行路径并发,控制台输出交错混排(本次更新新增)

18. 实现多线程方式三:实现 Callable 接口 + FutureTask(CallableImpl 与 ThreadTest03)

概念 —— Java 多线程的第三种实现方式(可返回结果)

前两种方式(继承 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

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

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

三种实现方式对比(Demo04 更新)

// 来源: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

Thread 类的其他常用方法(Demo04 更新)

// 来源:course/Demo04.java(注释部分,本次更新新增)
/*
    Thread类的其他常用的方法:
        setName(String name)    为这个线程设置线程名
        String getName()    获取这个线程的线程名
        Thread currentThread()  返回当前正在执行的线程对象的引用
*/

要点:

  • setName(String) / getName():实例方法,设置 / 获取当前线程对象的线程名(等价于构造方法传 name 的方式)
  • Thread.currentThread():静态方法,返回当前正在执行这段代码的线程对象的引用——在实现 Runnable / Callable 接口时(this 不是线程),用它拿线程对象再调 getName() 等实例方法

19. 知识点全景总结

本课知识点围绕"字符集转码 + 对象持久化 + 多线程入门"展开,一张表看清"知识 → 代码位置":

知识点 具体体现 源码位置
编码 / 解码模型 字符--编码-->字节--解码-->字符;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

20. 随堂练习要点

  • 转换流转码练习:参考 Demo01 main,把 D:/abc.txt(UTF-8)转码为 GBK 的 D:/hello.txt,再用 InputStreamReader(...,"GBK") 读回验证内容正确;改错字符集(如读端写 UTF-8)观察乱码现象
  • 指定字符集读写练习:用 InputStreamReader + OutputStreamWriter 完成"读 GBK 文件 → 写 UTF-8 文件",体会两个构造参数 String charsetName 的作用
  • 字符流 vs 转换流对照:上一日 FileReader/FileWriter 只能使用默认字符集;改用 InputStreamReader/OutputStreamWriter 手动指定字符集,对比异同,理解"FileReader = 字节流 + 默认字符集"
  • 序列化基础练习:仿照 ObjectOutputStreamTest 写端(取消注释),把 Student 对象序列化到 D:/studentObject.txt,再运行 ObjectInputStreamTest 反序列化读回,观察控制台输出
  • transient 验证练习:序列化后再反序列化,观察 Student 的 id 输出是否为 null(因为 id 被 transient 修饰),验证"transient 属性不参与序列化"
  • serialVersionUID 练习:序列化 Student 对象后,修改 Student 类(如注释/新增属性 sex),再反序列化:先去掉 serialVersionUID 看是否报 InvalidClassException,再加上 serialVersionUID 对比验证兼容效果
  • readObject 强转练习:把 ObjectInputStreamTest 中 Object o = ois.readObject() 改为 Student s = (Student) ois.readObject();,强转后调用 s.getName() 等 getter 方法
  • 对象数组 / 多个对象序列化:尝试连续 writeObject() 序列化多个 Student 对象,再连续 readObject() 读回,体会字节序列的顺序性
  • 序列化注意事项验证:①给 Student 加一个 static 字段,序列化 / 反序列化后验证其值不受影响(静态属性不参与序列化);②让 Student 引用一个未实现 Serializable 的类(如 Address),序列化观察 NotSerializableException 指向引用链上哪个类;③在构造方法中加打印语句,反序列化时观察是否触发(验证"反序列化不调用构造方法")
  • 父类序列化练习:定义一个实现 Serializable 的父类和一个未显式声明的子类,直接序列化子类对象,验证"父类实现 → 子类自动可序列化"
  • 单例 readResolve() 练习:给单例类实现 Serializable,序列化后再反序列化,打印两次对象 hashCode 对比是否为同一实例;重写 readResolve() 返回既有单例后再次验证
  • IO 流体系梳理练习:仿照 Demo03 小结,自己画出完整 IO 流体系图(基础流:字节 / 字符流;处理流:缓冲流 / 转换流 / 对象流),并说出每个流"处理什么、怎么配对包装、有什么特有方法"
  • 课后作业复习:运行 homework0807/Demo_03,用同一个大文件对比 Copy01(普通字节流)与 BufferedCopy(缓冲流)的复制耗时,验证缓冲流效率更高;再把 Copy01 的 try-catch-finally 与 BufferedCopy 的 throws 两种异常处理风格对比
  • 继承 Thread 创建线程:仿照 MyThread 自定义一个线程类(继承 Thread + 重写 run()),在 run() 中循环打印 0~999;创建两个线程对象并 start() 启动,观察两个线程的输出交替出现,体会并发执行
  • run() 与 start() 对比实验:分别用 mt.run() 和 mt.start() 启动同一个线程,观察输出区别——直接调 run() 时只有一个线程顺序执行(先执行完 run 再走下一步),而 start() 启动的是独立线程
  • 多线程求和实验:在 run() 中做 1~10000 求和并打印,启动多个线程,观察每个线程的求和结果是否正确(当前代码无共享数据,各线程独立计算)
  • 主线程实验:在 main 中先启动两个子线程再打印 "main 主线程" 内容,观察"主线程输出"与"子线程输出"的先后顺序(由 CPU 调度决定,顺序不定)
  • 概念梳理:用"程序 / 进程 / 线程"三个词解释:Java 源文件(.java)、运行中的 JVM、main 方法与 start() 出来的子线程,各自属于哪个概念
  • 实现 Runnable 接口:仿照 RunnableImpl 定义一个实现类(implements Runnable + 重写 run()),创建两个 Thread 对象并指定不同线程名,start() 启动,观察两个线程输出交错;用 Thread.currentThread().getName() 打印当前线程名
  • 三种提供 Runnable 的方式:在 ThreadTest02 中分别用实现类对象、匿名内部类、Lambda 表达式创建三个线程(光头强 / 熊大 / 熊二),体会三种写法从繁琐到简洁的演进,重点理解 Lambda 为什么能替代匿名内部类(Runnable 是函数式接口)
  • 方式一 vs 方式二对比:分别用继承 Thread 和实现 Runnable 写一个"打印 1~100"的线程任务,对比两种写法的类结构;思考为什么方式二更推荐(提示:Java 单继承、接口多实现、任务与线程解耦)
  • 线程名实验:用 new Thread(task, "自定义名") 指定线程名后,在 run() 中用 Thread.currentThread().getName() 打印,验证拿到的是构造时指定的名字;不指定名字时观察默认线程名(如 Thread-0 / Thread-1)
  • 改造练习:把 MyThread(继承 Thread 方式)改造成"实现 Runnable 方式"的 MyRunnable,再在 ThreadTest02 中用实现类对象方式启动,体会两种方式代码结构的不同
  • 实现 Callable 接口:仿照 CallableImpl 定义一个实现 Callable<Integer> 的类,重写 call() 计算 1~N 的和并返回;用 FutureTask 包装后启动线程,get() 打印返回值
  • get() 等待机制实验:在 call() 中加 Thread.sleep() 模拟耗时计算,观察 get() 调用前主线程是否被阻塞(get 会等待计算完成);比较"调用 get() 前打印"和"调用 get() 后打印"的输出顺序
  • 三种方式完整对比:分别用继承 Thread、实现 Runnable、实现 Callable 各写一个"求 1~100 的和"的线程任务,对比三种代码结构(有无返回值、能否继承其他类、能否直接用 getName())
  • 主线程并发实验:运行更新后的 ThreadTest02,观察控制台是否同时出现 "main"、"光头强线程!"、"熊大线程!"、"熊二线程!" 四类输出交错混排,验证主线程与子线程并发执行
  • Thread 常用方法练习:用 setName("新名字") 动态修改线程名,再用 getName() 打印验证;在 main 中调用 Thread.currentThread().getName() 确认主线程名是 "main"

21. 拓展阅读

  • JDK 文档:java.io.InputStreamReader、java.io.OutputStreamWriter、java.io.ObjectOutputStream、java.io.ObjectInputStream、java.io.Serializable
  • 字符集与乱码:文件的"保存编码"与"读取编码"必须一致;FileReader / FileWriter 只能使用默认字符集,而 InputStreamReader / OutputStreamWriter 可以手动指定字符集,是处理跨字符集文件的利器;常用的 UTF-8 是 Unicode 的一种可变长实现,兼容 ASCII
  • 装饰器模式:转换流、缓冲流、对象流都是"装饰器模式"在 Java I/O 中的体现——用对象包装对象、一层套一层(ObjectOutputStream(BufferedOutputStream(FileOutputStream(...)))),每层各加一种能力(缓冲 / 转码 / 序列化)
  • 标记接口(Marker Interface):Serializable、Cloneable 等接口没有任何方法,仅作为"能力标识";instanceof Serializable 判断对象是否可序列化
  • serialVersionUID 规范:强烈建议所有实现 Serializable 的类显式声明 private static final long serialVersionUID = 1L;,否则类一旦修改结构,JVM 自动生成版本号变化会导致反序列化抛 InvalidClassException
  • transient 与 static:static 修饰的成员属于类、不属于对象,本身就不参与序列化;transient 用于让实例成员也跳过序列化
  • 序列化与继承:父类实现 Serializable 则子类自动可序列化;若父类未实现 Serializable,则父类部分字段不参与序列化,且反序列化时会调用父类的无参构造器(对比:子类部分反序列化不调用构造方法)
  • NotSerializableException:引用链上任一对象未实现 Serializable 就会抛此异常,异常信息会点名是哪个类——排查时顺着引用链逐级检查
  • readResolve() 与单例保护:反序列化是"绕过构造器创建新实例"的旁门左道;readResolve() 是单例模式对抗序列化的标准手段——在 readResolve() 中返回内存中既有的单例实例,保证全局唯一
  • Externalizable:除 Serializable 外还有 Externalizable 接口,实现 writeExternal() / readExternal() 可完全自定义序列化内容(比 Serializable 更可控但需手动实现)
  • 网络传输对象:对象持久化不只用于本地磁盘文件,还广泛用于 RMI、RPC、消息队列 等场景——对象跨网络传输的本质也是"对象 → 字节序列 → 网络 → 字节序列 → 对象"
  • 更现代的序列化方案:Java 原生序列化写出的文件是二进制、且不够高效;生产环境常改用 JSON(Jackson / Gson / Fastjson)、XML 等文本格式,或 Protobuf 等二进制协议做对象持久化 / 传输
  • IO 流学习路线:File(路径管理)→ 字节流 → 字节缓冲流 → 字符流 → 字符缓冲流 → 转换流(字符集转码) → 对象流(对象持久化 / 序列化),后面还可学打印流(PrintStream / PrintWriter)、数据流(DataInputStream / DataOutputStream)等
  • JDK 文档:java.lang.Thread(线程类,run() / start() / getName() 等核心方法)
  • 多线程实现方式:本日学了方式一(继承 Thread)与方式二(实现 Runnable 接口);后续还有方式三(实现 Callable 接口 + FutureTask,可返回结果)、线程池(ExecutorService) 等——面向接口实现 Runnable 更推荐,因为 Java 是单继承,实现接口可以保留继承其他类的机会
  • Runnable 的函数式编程:Runnable 是函数式接口(只有一个抽象方法 run()),所以可以用 Lambda 表达式 new Thread(() -> {...}) 或方法引用简化匿名内部类——这是 08-04 学过的函数式编程在并发编程中的应用
  • Thread.currentThread():方式二中没有继承 Thread,无法直接 this.getName();Thread.currentThread() 是静态方法,返回当前正在执行该行代码的线程对象,配合 getName() / setName() / getPriority() 使用
  • 方式一 vs 方式二:方式一"继承 Thread"类本身就是线程(强耦合);方式二"实现 Runnable"把任务与线程解耦——同一个 Runnable 对象可以交给多个 Thread 执行(共享任务),是官方更推荐的做法
  • 共享任务的线程安全问题:同一个 Runnable 对象交给多个线程执行时,run() 中如果有共享成员变量就会产生数据竞争(如多线程卖票票数自减);后续将用 synchronized 等机制解决
  • JDK 文档:java.util.concurrent.Callable、java.util.concurrent.FutureTask、java.util.concurrent.Future
  • Future 接口:FutureTask 实现了 Future 接口——get() 获取结果、isDone() 判断任务是否完成、cancel() 取消任务;Future 就是"异步计算的结果占位符",体现了异步编程思想
  • Callable 的线程池应用:ExecutorService 线程池的 submit(Callable) 方法直接返回 Future 对象,是生产环境"提交带返回值的任务"的主流写法(比手动 new Thread + FutureTask 更规范)
  • 三种方式到线程池:继承 Thread(直接 new)、实现 Runnable(配合 Executor)、实现 Callable(submit 返回 Future)——学习路线最终都会走向线程池统一管理线程(复用、控制并发数、避免频繁创建销毁线程的开销)
  • 线程生命周期:线程有新建(New)→ 就绪(Runnable)→ 运行(Running)→ 阻塞(Blocked)→ 死亡(Terminated)等状态;start() 只是让线程进入就绪,具体运行由 CPU 调度
  • 线程安全问题:多个线程同时访问共享数据时会出现数据不一致(如两个线程同时给同一个变量自增);解决手段有 synchronized 同步代码块 / 同步方法、Lock 锁、volatile 等(后续课程学习)
  • 线程常用方法:sleep(毫秒)(让线程休眠)、join()(等待线程执行完)、yield()(让出 CPU)、setName/getName(设置/获取线程名)、isAlive()(判断线程是否存活)等
  • 并发编程参考:JDK 提供了 java.util.concurrent 并发包(ThreadPoolExecutor、ConcurrentHashMap、CountDownLatch 等),是生产环境多线程编程的主流工具