Explorar o código

feat: 新增20260808课程内容——对象序列化/反序列化(ObjectInputStream/ObjectOutputStream)与转换流字符集转码,多线程入门(MyThread继承Thread/RunnableImpl实现Runnable/CallableImpl实现Callable+FutureTask,ThreadTest01~03三种实现方式对比),0807课后作业参考解答(homework0807接口化文件复制效率对比器),含课后作业(多线程三种实现方式)/课堂笔记/课堂练习

WanJL hai 1 semana
pai
achega
0df9dd0b83

+ 30 - 0
授课代码/c260808/.gitignore

@@ -0,0 +1,30 @@
+### IntelliJ IDEA ###
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+.kotlin
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store

+ 23 - 0
授课代码/c260808/src/course/CallableImpl.java

@@ -0,0 +1,23 @@
+package course;
+
+import java.util.concurrent.Callable;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title CallableImpl
+ * @description
+ * @create 2026/8/8
+ */
+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;
+    }
+}

+ 56 - 0
授课代码/c260808/src/course/Demo01.java

@@ -0,0 +1,56 @@
+package course;
+
+import java.io.*;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Demo01
+ * @description 转换流
+ * @create 2026/8/8
+ */
+public class Demo01 {
+    /*
+        我们讲字符流的时候,说过字符集的概念,
+        不同的字符集虽然都能转换成字节(二进制数)但是,同一个字节,在不同的字符集中表示的含义是不一样的。
+
+        字符--编码-->字节--解码-->字符
+        如果使用字符集A进行编码,使用字符集B进行解码,就会出现字符集乱码。
+
+        但是如果出现这样情况:
+            我们对于一个文件需要追加写入一些内容。但是这个文件本身是使用GBK编码的。
+            而我们只能使用UTF-8字符集。那么怎么处理?
+            我们可以使用转换流,对A字符集的文件,进行转换,转为B字符集的文件。
+
+            InputStreamReader:是字节流到字符流的桥梁,父类是Reader
+                    他是读取字节,并且使用指定的字符集把它解码为字符。
+                    它使用的字符集可以有名称指定,也可以直接使用平台默认字符集。
+            OutputStreamWriter:是字符流到字节流的桥梁,父类是Writer
+                    它是读取字符,使用指定的字符集把它编码为字节。
+            构造方法:
+                public InputStreamReader(InputStream in)    使用默认字符集创建InputStreamReader对象
+                public InputStreamReader(InputStream in, String charsetName) 使用charsetName指定的字符集创建InputStreamReader对象
+                public OutputStreamWriter(OutputStream out) 使用默认字符集创建OutputStreamWriter对象
+                public OutputStreamWriter(OutputStream out, String charsetName) 使用charsetName指定的字符集创建OutputStreamWriter对象
+
+            案例:
+                使用UTF-8字符集打开abc.txt文件,并且转换成GBK字符集的hello.txt文件
+     */
+
+    public static void main(String[] args) throws IOException {
+        //使用UTF-8字符集打开abc.txt文件
+        InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\abc.txt"),"UTF-8");
+
+        //把内容使用GBK字符集写入到hello.txt文件
+        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\hello.txt"),"GBK");
+
+        char[] chars=new char[1024];
+        int count;
+        while ((count=isr.read(chars))!=-1){
+            osw.write(chars,0,count);
+        }
+        osw.close();
+        isr.close();
+    }
+
+}

+ 85 - 0
授课代码/c260808/src/course/Demo02.java

@@ -0,0 +1,85 @@
+package course;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Demo02
+ * @description 对象持久化
+ * @create 2026/8/8
+ */
+public class Demo02 {
+    /*
+        我们可以把一个类文件分享给其他人,但是如果是根据某个类创建的对象,想要分享给其他人,就不容易了
+        因为它是程序运行时,在内存中开辟空间创建的对象。
+        但是我们可以通过【对象持久化】的方式,把内存中的对象,保存到磁盘中,分享其他人。
+        对象持久化:就是把对象保存到磁盘、或者在网络中传输对象。
+        这种机制就是使用一个字节序列表示一个对象,这个字节序列包含:
+        对象的类型
+        对象的数据,和对象中存储的属性等等信息。
+        把字节序列写入到文件中后,就相当于永久的保存了一个对象。
+        反过来,可以使用【对象反序列化】的方式再从文件中读取出对象,进行对象重构。
+        进行对象序列化的方式就是使用【对象序列化流】    ObjectOutputStream
+        构造方法:
+        public ObjectOutputStream(OutputStream out) 创建一个写入指定OutputStream的对象序列化流对象
+
+        对象序列化的方法:
+            public final void writeObject(Object obj)   将指定对象obj写入到ObjectOutputStream
+
+        一个对象想要被序列化,必须要实现一个接口 java.io.Serializable
+        Serializable接口没有任何需要重写的方法,也没有任何属性,它就像一个标识,标志着这个类的对象可以被序列化。
+
+
+        进行对象反序列化的方式就是使用【对象反序列化流】 ObjectInputStream
+        ObjectInputStream是把对象序列化后的文件进行反序列化,返回对象。
+        public ObjectInputStream(InputStream in)    创建从InputStream中读取的反序列化对象
+        public final Object readObject() 从ObjectInputStream读取一个对象
+
+
+        如果 第1天 张三,通过Student类,创建s对象,使用对象序列化把对象持久化保存,并且发送给李四。
+        李四收到序列化的文件。
+        张三 修改了Student类,修改了类的属性,比如增加了一个sex性别,移除了age年龄。
+        李四再进行反序列化,会得到什么?
+            答:会报错,抛异常:java.io.InvalidClassException
+        怎么解决?
+            方案一:重新序列化
+            方案二:为对象所属的类增加一个属性:
+            private static final long serialVersionUID =42L;
+
+        完成,再进行序列化后,修改属性,再反序列化的时候,就不会报错抛异常,而是正常显示,
+        有这个属性,就正常赋值,没有就赋值默认值,比如:
+        Student{id=1001, name='张三', age=null}
+
+        serialVersionUID的作用:
+            1、用来验证反序列化的时候类的版本一致性
+            2、如果我们没有自己定义这个属性,那么JVM会根据类型的结构自己定义serialVersionUID。
+            3、如果JVM定义serialVersionUID,然后类的结构发生了改变,就会导致serialVersionUID值也发生变化。
+            Student类--JVM假设 定义的serialVersionUID是123456L,序列化对象,对象的serialVersionUID也是123456L
+            Stduent类的结构发生了改变,serialVersionUID值也会发生改变,比如变为 456789L。
+            再进行反序列化的时候,对比两个serialVersionUID值发现不一致,就会出现异常java.io.InvalidClassException
+            最好的方式就是我们自己显式声明java.io.InvalidClassException属性,至于是什么值,无所谓。
+            private static final long serialVersionUID =42L;
+
+        序列化的注意事项:
+            1、静态属性不能序列化,因为静态属性是属于类的,不是属于某个对象的。
+            2、transient修饰的属性不能序列化
+            3、父类实现了java.io.Serializable接口,那么子类可以自动序列化。
+            4、反序列化不会调用构造方法
+            5、如果对象引用了其他的对象,那么引用链上的所有的对象必须是可以序列化的。
+            6、单例对象需要重写一个方法readResolve()方法
+
+
+
+        如果类中有某个属性不行被序列化,可以给这个成员添加transient关键字修饰:
+        private transient Integer id;
+        那么在序列化的时候,这个属性。就不会参与序列化。
+
+     */
+
+
+
+}

+ 54 - 0
授课代码/c260808/src/course/Demo03.java

@@ -0,0 +1,54 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Demo03
+ * @description IO流总结
+ * @create 2026/8/8
+ */
+public class Demo03 {
+    /*
+        基础流:
+            字节流:
+                字节输入流:InputStream
+                    |-文件字节输入流:FileInputStream
+                字节输出流:OutputStream
+                    |-文件字节输出流:FileOutputStream
+            字符流:
+                字符输入流:Reader
+                    |-文件字符输入流:FileReader
+                字符输出流:Writer
+                    |-文件字符输出流:FileWriter
+         处理流:
+            缓冲流:
+                缓冲字节流:
+                    缓冲字节输入流:BufferedInputStream
+                    缓冲字节输出流:BufferedOutputStream
+                缓冲字符流:
+                    缓冲字符输入流:BufferedReader
+                    缓冲字符输出流:BufferedWriter
+            转换流:
+                输入:InputStreamReader---把字节流根据指定的字符集转换为字符
+                输出:OutputStreamWriter--把把字符根据指定的字符集转化为字节流
+
+            对象流:
+                序列化:ObjectOutputStream
+                反序列化:ObjectInputStream
+
+         常用的方法:
+            write()     把内存中的数据写入到文件
+            read()     从文件中读取数据到内存
+            flush()     强制刷新缓冲流缓冲区的数据到文件
+            close()     关闭流,释放资源
+        特殊方法:
+            readLine()  读取一行数据,缓冲输入字符流特有的
+            newLine()   根据不同的系统进行一次换行。
+            writeObject()   对象序列化--把内存中的对象保存到文件中
+            readObject()    反序列化--从文件中读取并在内存中重构对象,不走构造方法。
+        特殊属性:
+            private static final long serialVersionUID  序列化的版本UID,用来验证反序列化和序列化的类版本一致性
+        特殊的关键字:
+            transient 修饰属性,让属性不参与对象序列化
+     */
+}

+ 111 - 0
授课代码/c260808/src/course/Demo04.java

@@ -0,0 +1,111 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Demo04
+ * @description 多线程
+ * @create 2026/8/8
+ */
+public class Demo04 {
+    /*
+        什么是多线程?
+            是指从软件或硬件上实现多个线程并发执行的技术。
+            具有多线程能力的计算机因为硬件支持所以能够同一时间执行多个线程,提高性能。
+        什么线程?什么叫进程?
+            我们知道,程序本质上就是一种文件,它是存储在硬盘中,程序想要被执行,就需要被加载到内存中执行。
+            在内存中执行的着的程序就是进程。
+            一个程序运行起来后,就成了进程,需要占用CPU、内存、网络、GPU、磁盘等等....
+            一个进程中可能包含多个线程。
+            比如:360安全卫士,同时进行木马查杀、电脑清理、系统修复、优化加速,每使用一个功能,就是启动一个线程。
+            进程启动线程后,都是交给CPU(计算核心(运算器和控制器))进行处理
+
+            线程的并行和并发
+            并行:同一个时刻,有多个指令在多个CPU上同时执行。比如:饭店后厨,有3个灶台被三位厨师使用炒菜。
+            并发:同一时刻,有多个指令在单个CPU上交替执行。比如:饭店后厨,有3个灶台,被一位厨师炒菜。
+
+
+            CPU 计算机核心
+                红色警戒    没有GPU,全靠CPU
+                特性 适合进行复杂运算,但是不适合进行简单的重复性大量运算
+            GPU 显卡核心
+
+            比特币挖矿   CPU不适合挖矿,比特币本质上就是一种计算机运算,
+            运算难度不高,复杂程度也不高,但是量大。
+            3A游戏,只狼、荒野大镖客、GTA5、MC
+
+            UI设计
+
+            AI  大模型--->GPU
+            满血大模型
+            本地部署AI大模型,也是吃GPU
+
+            进程:就是正在运行的程序
+                具有独立性:进程是一个能够独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
+                具有动态性:进程的本质就是程序的一次执行过程。进程是动态产生动态消亡。
+                具有并发性:任何进程都可以和其他进程一起并发执行。
+            线程:
+                是进程中的单个顺序控制流,是一条执行路径。
+                单线程:一个进程如果只有一条路径,那么它就是单线程程序。
+                多线程:一个进程如果有多条路径,那么它就是多线程程序。
+
+
+
+         Java中实现多线程的方式:
+            方式一: 继承 Thread 类,重写 run()方法。
+                实现步骤:
+                    1、自定义一个类,继承Thread类
+                    2、在自定义类中重写父类的run()方法,设置在线程中要执行的操作。
+                    3、创建自定义线程类的对象
+                    4、调用自定义线程类的start()方法 启动线程。
+
+                为什么要重写 run()方法?
+                    因为run()方法是用来封装被线程执行的代码。
+                run()方法和start()方法有什么区别?
+                    run()方法是用来封装被线程执行的代码,如果直接调用run(),不会创建新的线程,而是和普通方法一样的调用。
+                    start()方法是启动线程的方法,调用这个方法后会启动一个新的线程,然后在线程中由JVM调用这个线程的run()方法
+
+            方式二:实现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()方法启动线程
+
+           方式二相比于方式一,更推荐,因为可以实现多个接口,并且可以继承其他类,不会改变程序类的上下文语义。
+
+             方式三:实现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方法,获取线程结束后的结果。
+
+               三种实现方式的对比:
+                    实现Runnable、Callable接口
+                        好处:扩展性更强,实现这个接口的同时也可以实现其他接口以及继承其他父类。
+                        缺点:编程相对更复杂,不能直接使用Thread类中的方法
+                    继承Thread类
+                        好处:编程比较简单,可以直接使用Thread类中的方法
+                        缺点:可扩展性比较差,不能再继承其他类。
+                Thread类的其他常用的方法:
+                    setName(String name)    为这个线程设置线程名
+                    String getName()    获取这个线程的线程名
+                    Thread currentThread()  返回当前正在执行的线程对象的引用
+
+
+     */
+
+}

+ 15 - 0
授课代码/c260808/src/course/Hello.java

@@ -0,0 +1,15 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Hello
+ * @description
+ * @create 2026/8/8
+ */
+public class Hello {
+    //main方法被称为主方法,也就是Java程序的主线程。
+    public static void main(String[] args) {
+        System.out.println("Hello World");
+    }
+}

+ 29 - 0
授课代码/c260808/src/course/MyThread.java

@@ -0,0 +1,29 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title MyThread
+ * @description 自定义的线程类,必须要继承Thread类,它才能是线程类
+ * @create 2026/8/8
+ */
+public class MyThread extends Thread{
+    public MyThread(String name) {
+        super(name);
+    }
+
+    /**
+     * 重写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....我的自定义线程....");
+    }
+}

+ 22 - 0
授课代码/c260808/src/course/ObjectInputStreamTest.java

@@ -0,0 +1,22 @@
+package course;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title ObjectInputStreamTest
+ * @description
+ * @create 2026/8/8
+ */
+public class ObjectInputStreamTest {
+    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);
+    }
+}

+ 27 - 0
授课代码/c260808/src/course/ObjectOutputStreamTest.java

@@ -0,0 +1,27 @@
+package course;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title ObjectOutputStreamTest
+ * @description
+ * @create 2026/8/8
+ */
+public class ObjectOutputStreamTest {
+//    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();
+//    }
+}

+ 22 - 0
授课代码/c260808/src/course/RunnableImpl.java

@@ -0,0 +1,22 @@
+package course;
+
+import java.io.Serializable;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title RunnableImpl
+ * @description
+ * @create 2026/8/8
+ */
+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);
+        }
+    }
+}

+ 106 - 0
授课代码/c260808/src/course/Student.java

@@ -0,0 +1,106 @@
+package course;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Student
+ * @description Student类型的对象想要被序列化,就必须要实现Serializable接口
+ * @create 2026/8/8
+ */
+public class Student implements Serializable {
+    //增加一个属性
+    private static final long serialVersionUID =42L;
+
+
+
+    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 +
+                '}';
+    }
+
+//
+//    public String getSex() {
+//        return sex;
+//    }
+//
+//    public void setSex(String sex) {
+//        this.sex = sex;
+//    }
+//
+//    @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(sex, student.sex);
+//    }
+//
+//    @Override
+//    public int hashCode() {
+//        return Objects.hash(id, name, sex);
+//    }
+//
+//    @Override
+//    public String toString() {
+//        return "Student{" +
+//                "id=" + id +
+//                ", name='" + name + '\'' +
+//                ", sex='" + sex + '\'' +
+//                '}';
+//    }
+}

+ 21 - 0
授课代码/c260808/src/course/ThreadTest01.java

@@ -0,0 +1,21 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title ThreadTest
+ * @description
+ * @create 2026/8/8
+ */
+public class ThreadTest01 {
+    public static void main(String[] args) {
+
+        //创建自定义线程类的对象
+        MyThread mt1=new MyThread("线程1");
+        MyThread mt2=new MyThread("线程2");
+        //调用自定义线程类的start()方法 启动线程。
+        mt1.start();
+        mt2.start();
+
+    }
+}

+ 46 - 0
授课代码/c260808/src/course/ThreadTest02.java

@@ -0,0 +1,46 @@
+package course;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title ThreadTest02
+ * @description
+ * @create 2026/8/8
+ */
+public class ThreadTest02 {
+    public static void main(String[] args) {
+        //创建实现类对象
+        RunnableImpl ri=new RunnableImpl();
+        //创建Thread类的对象,把实现类对象作为参数传入Thread的构造方法
+        Thread t1=new Thread(ri,"光头强线程!");
+        //匿名内部类
+        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表达式
+        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();
+
+        String name = Thread.currentThread().getName();
+        for (int i = 0; i < 10000; i++) {
+            System.out.println("我是"+name+"--->"+i);
+        }
+
+    }
+}

+ 42 - 0
授课代码/c260808/src/course/ThreadTest03.java

@@ -0,0 +1,42 @@
+package course;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title ThreadTest03
+ * @description
+ * @create 2026/8/8
+ */
+public class ThreadTest03 {
+    public static void main(String[] args) throws ExecutionException, InterruptedException {
+        //创建自定义实现类的对象
+        CallableImpl ci=new CallableImpl();
+        //创建FutureTask对象,并且把自定义实现类对象作为参数传入构造方法。
+        FutureTask<Integer> ft1=new FutureTask<>(ci);
+        FutureTask<Integer> ft2=new FutureTask<>(ci);
+        FutureTask<Integer> ft3=new FutureTask<>(ci);
+        //创建Thread类的对象,把FutureTask对象作为参数传入构造方法
+        Thread t1=new Thread(ft1,"线程1");
+        //还是之前的任务ft,创建新的线程
+        Thread t2=new Thread(ft2,"线程2");
+        Thread t3=new Thread(ft3,"线程3");
+
+        //用Thread类的对象的start()方法,启动线程
+        t2.start();
+        t3.start();
+        t1.start();
+        //可以调用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);
+    }
+}

+ 91 - 0
授课代码/c260808/src/homework0807/Demo_03.java

@@ -0,0 +1,91 @@
+package homework0807;
+
+import java.io.*;
+
+/**
+ * @author WanJl
+ * @version 1.0
+ * @title Demo_03
+ * @description 题目3:接口化文件复制效率对比器
+ * @create 2026/8/8
+ */
+public class Demo_03 {
+    // ========== ① 复制器接口:统一复制规范 ==========
+    interface CopyFile {
+        String copy(String src, String desc) throws IOException;
+    }
+
+    // ========== ② 普通字节流实现:FileInputStream + FileOutputStream + byte[8192] + try-catch-finally ==========
+    static class Copy01 implements CopyFile {
+        @Override
+        public String copy(String src, String desc) {
+            // TODO 1: FileOutputStream fos / FileInputStream fis 声明为 null,try 中创建
+            FileOutputStream fos = null;
+            FileInputStream fis = null;
+            try {
+                fos = new FileOutputStream(desc);
+                fis = new FileInputStream(src);
+                // TODO 2: byte[8192] 缓冲区 + while ((len = fis.read(bytes)) != -1) { fos.write(bytes, 0, len); }
+                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) {
+                // TODO 3: catch (IOException e) 返回 "文件复制失败"
+                return "文件复制失败";
+            } finally {
+                // TODO 4: finally 中关闭 fos / fis(close 也可能抛 IOException → 包装成 RuntimeException)
+                try {
+                    fos.close();
+                    fis.close();
+                } catch (IOException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+            return "文件复制成功";
+        }
+    }
+
+    // ========== ③ 缓冲流实现:BufferedOutputStream / BufferedInputStream 包装底层流 ==========
+    static class BufferedCopy implements CopyFile {
+        @Override
+        public String copy(String src, String desc) throws IOException {
+            // TODO 5: 先创建文件流,再用缓冲流包装(new BufferedOutputStream(fos))
+            FileOutputStream fos = new FileOutputStream(desc);
+            FileInputStream fis = new FileInputStream(src);
+            BufferedOutputStream bos = new BufferedOutputStream(fos);
+            BufferedInputStream bis = new BufferedInputStream(fis);
+            // TODO 6: byte[8192] + while ((count = bis.read(bytes)) != -1) { bos.write(bytes, 0, count); }
+            byte[] bytes = new byte[8192];
+            int len;
+            while ((len = bis.read(bytes)) != -1) {
+                bos.write(bytes, 0, len);
+            }
+            // TODO 7: 关闭:先关缓冲流再关底层流
+            bos.close();
+            bis.close();
+            fos.close();
+            fis.close();
+            return "复制成功";
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        String src = "D:\\CentOS7\\kyl-CentOS7.zip";   // 换成你电脑上任意大于 10MB 的文件
+        // TODO 8: 用 new Copy01().copy(src, "D:/copy1.zip") 复制,System.currentTimeMillis() 统计耗时
+        long start1 = System.currentTimeMillis();
+        new Copy01().copy(src, "D:/copy1.zip");
+        long end1 = System.currentTimeMillis();
+        System.out.println("耗时:"+(end1-start1));
+        // TODO 9: 用 new BufferedCopy().copy(src, "D:/copy2.zip") 复制,统计耗时
+        long start2 = System.currentTimeMillis();
+        new BufferedCopy().copy(src, "D:/copy2.zip");
+        long end2 = System.currentTimeMillis();
+        System.out.println("耗时:"+(end2-start2));
+
+        // TODO 10: 输出两次耗时,对比哪个更快
+    }
+}

+ 251 - 0
课后作业/2026年8月8日-作业.md

@@ -0,0 +1,251 @@
+# 2026年8月8日 课后作业 — 多线程三种实现方式:继承 Thread / 实现 Runnable / 实现 Callable
+
+> **说明**:本次课程学习了 **多线程** 的核心内容——**进程 / 线程 / 并行与并发** 基础概念(`main` 方法即主线程)、**实现多线程的三种方式**:
+>
+> 1. **方式一:继承 `Thread` 类,重写 `run()`**(`super(name)` 构造器传线程名、`getName()`、`start()` 启动、`run()` 与 `start()` 的区别)
+> 2. **方式二:实现 `Runnable` 接口**(`Thread.currentThread().getName()`、`Thread(Runnable, name)` 构造方法、实现类对象 / 匿名内部类 / Lambda 三种提供任务的方式、主线程与子线程并发)
+> 3. **方式三:实现 `Callable` 接口 + `FutureTask`**(`call()` **带返回值**、泛型指定返回类型、`FutureTask` 包装、`get()` 获取计算结果)
+>
+> 以及 **三种实现方式的对比**(接口派 vs 继承派)与 **Thread 常用方法**(`setName` / `getName` / `currentThread`)。
+>
+> 本作业共 **3 道题**:**基础题(必做)**——方式一继承 Thread 多线程求和打印器;**进阶题(必做)**——方式二实现 Runnable 三种方式提供任务;**挑战题(选做)**——方式三实现 Callable + FutureTask 带返回值求和。覆盖多线程核心知识点。
+
+---
+
+## 📌 作业总体要求
+
+### 一、命名要求
+
+| 项目 | 要求 |
+|------|------|
+| **类名** | `Demo_题号_你的姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) |
+| **文件头注释** | 每个 Java 文件头部需包含 `@author`、`@title`、`@create` 文档注释 |
+
+### 二、提交方式与截止时间
+
+| 项目 | 要求 |
+|------|------|
+| **提交方式** | 将源码文件 + 运行结果截图提交至班级指定平台 |
+| **截止时间** | 下次上课前 |
+
+---
+
+## 一、基础题(必做)
+
+### 题目 1:继承 Thread 多线程求和打印器 ThreadSumDemo(⭐⭐ 必做)
+
+**知识点**:进程 / 线程概念、方式一**继承 `Thread` 类**、重写 `run()` 封装线程操作、`super(name)` 构造器传线程名、`getName()` 获取线程名、`start()` 启动线程、`run()` vs `start()` 的区别
+
+**场景描述**:课堂用 `MyThread` 演示了方式一(继承 Thread)——自定义线程类在 `run()` 中边求和边打印,`ThreadTest01` 创建两个线程对象用 `start()` 启动,两个线程的输出**交错出现**。请仿照实现:两个线程各自从 1 累加到 100,边加边打印线程名和当前值,最后打印各自的总和,观察并发执行效果。
+
+**题目要求**:
+
+```java
+/**
+ * @author 你的姓名
+ * @title 题目1:继承 Thread 多线程求和打印器
+ * @create 2026/8/8
+ */
+public class Demo_01_姓名 {
+    // ========== ① 自定义线程类:继承 Thread ==========
+    static class SumThread extends Thread {
+        public SumThread(String name) {
+            super(name);   // TODO 1: 把线程名传给父类 Thread
+        }
+        @Override
+        public void run() {
+            // TODO 2: int sum = 0; 从 1 累加到 100
+            // TODO 3: 边加边打印 this.getName() + "--->" + i
+            // TODO 4: 循环结束打印 this.getName() + "求和结果:" + sum
+        }
+    }
+
+    public static void main(String[] args) {
+        // TODO 5: 创建两个 SumThread 线程对象(如"加法线程1"、"加法线程2")
+        // TODO 6: 调用 start() 启动两个线程,观察输出是否交错出现
+        // TODO 7: (思考)把 start() 改成 run() 再运行一次,对比输出顺序有什么不同
+    }
+}
+```
+
+**预期输出示例**(每次运行顺序可能不同):
+
+```
+加法线程1--->1
+加法线程2--->1
+加法线程1--->2
+加法线程2--->2
+...
+加法线程1求和结果:5050
+加法线程2求和结果:5050
+```
+
+**要求说明**:
+1. 自定义类必须 `extends Thread`,重写 `run()`,在 `run()` 中封装"求和 + 打印"的操作
+2. 两个线程对象必须用构造方法传入不同线程名,用 `this.getName()` 打印
+3. 启动线程必须调用 `start()`,不允许直接调 `run()`
+
+**💡 提示**:
+- `super(name)` 把线程名传给 Thread 父类;构造后 `this.getName()` 就能拿到这个名字
+- 两个线程的 `start()` 会启动**两条独立执行路径**,CPU 交替调度它们,所以输出交错
+- 直接调 `run()` 不会创建新线程——它只是普通方法调用,会先执行完线程1的 run 再执行线程2的 run(验证 run 与 start 的区别)
+
+**🎯 挑战加分(选做)**:
+1. 把求和上限改成构造参数传入(如 `new SumThread("线程1", 1000)`),让两个线程求不同范围的和
+2. 用 `setName("新名字")` 在启动前动态修改线程名,再打印验证
+3. 用"程序 / 进程 / 线程"解释运行后:`Demo_01` 的 `.java` 文件、运行中的 JVM、两个 SumThread 线程各自对应什么概念
+
+---
+
+## 二、进阶题(必做)
+
+### 题目 2:实现 Runnable 三种方式提供任务 RunnableCountDemo(⭐⭐⭐ 必做)
+
+**知识点**:方式二**实现 `Runnable` 接口**、重写 `run()`、`Thread.currentThread().getName()`(不继承 Thread 时获取线程名)、`Thread(Runnable task, String name)` 构造方法指定线程名、**实现类对象 / 匿名内部类 / Lambda 表达式**三种提供任务的方式、`start()` 后**主线程不阻塞继续执行**(主线程与子线程并发)
+
+**场景描述**:课堂用 `RunnableImpl` + `ThreadTest02` 演示了方式二——`Runnable` 接口只有一个抽象方法 `run()`,是**函数式接口**,所以能用实现类对象、**匿名内部类**、**Lambda 表达式**三种方式提供任务;三个子线程 `start()` 后 **main 主线程也继续循环打印**,共 4 条执行路径并发。请完整实现这三种方式并观察主线程并发。
+
+**题目要求**:
+
+```java
+/**
+ * @author 你的姓名
+ * @title 题目2:实现 Runnable 三种方式提供任务
+ * @create 2026/8/8
+ */
+public class Demo_02_姓名 {
+    // ========== ① 实现 Runnable 接口的类 ==========
+    static class CountTask implements Runnable {
+        @Override
+        public void run() {
+            // TODO 1: String name = Thread.currentThread().getName(); 获取当前线程名
+            // TODO 2: 循环 100 次打印 "我是" + name + "--->" + i
+        }
+    }
+
+    public static void main(String[] args) {
+        // ========== ② 三种方式提供 Runnable ==========
+        // TODO 3: 方式一(实现类对象):new CountTask(),再 new Thread(countTask, "任务线程")
+        // TODO 4: 方式二(匿名内部类):new Thread(new Runnable(){ run(){...} }, "匿名线程")
+        // TODO 5: 方式三(Lambda):new Thread(() -> { ... }, "Lambda线程")
+        // TODO 6: 三个线程 start() 启动
+
+        // ========== ③ 主线程也参与打印(观察并发) ==========
+        // TODO 7: main 中用 Thread.currentThread().getName() 拿到主线程名并循环打印
+        // TODO 8: 观察控制台是否出现 4 种线程名(任务线程/匿名线程/Lambda线程/main)输出交错混排
+    }
+}
+```
+
+**预期输出示例**(每次运行顺序可能不同):
+
+```
+我是main--->0
+我是任务线程--->0
+我是匿名线程--->0
+我是Lambda线程--->0
+我是任务线程--->1
+我是main--->1
+...
+```
+
+**要求说明**:
+1. 必须实现 `Runnable` 接口(不允许继承 Thread),获取线程名用 `Thread.currentThread().getName()`
+2. 三种提供任务的方式都必须写出来:实现类对象、匿名内部类、Lambda 表达式
+3. 每个线程都要用 `Thread(Runnable, name)` 构造方法指定不同的线程名
+4. `main` 主线程也要循环打印,观察主线程与子线程并发(4 种线程名交错)
+
+**💡 提示**:
+- 实现 Runnable 的类 `this` 不是线程对象,所以用**静态方法** `Thread.currentThread()` 拿当前执行线程,再 `.getName()`
+- 匿名内部类写法:`new Thread(new Runnable() { @Override public void run() {...} }, "匿名线程")`
+- Lambda 写法:`new Thread(() -> { ... }, "Lambda线程")`——Runnable 是**函数式接口**(只有一个抽象方法)才能用 Lambda
+- `start()` 只是启动子线程,main 方法**不等待**、继续往下执行,所以主线程的循环打印会和子线程的输出混在一起
+
+**🎯 挑战加分(选做)**:
+1. 同一个 `CountTask` 对象同时传给两个 `Thread`,观察两个线程输出内容是否完全相同(体会"共享任务对象")
+2. 把循环次数改为 10000,观察四个线程的输出**交错密度**,理解并发切换
+3. 用 `Thread.sleep(1)`(需要 try-catch)在每次循环后休眠 1 毫秒,观察输出节奏变化
+
+---
+
+## 三、挑战题(选做)
+
+### 题目 3:实现 Callable + FutureTask 带返回值求和 SumCallableDemo(⭐⭐⭐⭐ 选做)
+
+**知识点**:方式三**实现 `Callable<V>` 接口**(泛型指定返回类型)、`V call()` **带返回值**且声明可抛异常、`FutureTask<V>(Callable<V>)` 包装任务、`new Thread(futureTask, name)` 启动、`V get()` 阻塞等待获取计算结果、**三种实现方式对比**(`run()` 无返回值 vs `call()` 有返回值)
+
+**场景描述**:前两种方式的 `run()` 方法**没有返回值**,线程计算完结果只能自己打印、无法交回主程序。方式三 `Callable.call()` **带返回值**:请定义 `SumCallable implements Callable<Integer>`,让 3 个线程分别计算 `1~100`、`1~1000`、`1~10000` 的和,用 `FutureTask.get()` 把每个线程的**计算结果取回**,最后在 main 中**汇总打印**。
+
+**题目要求**:
+
+```java
+import java.util.concurrent.*;
+
+/**
+ * @author 你的姓名
+ * @title 题目3:实现 Callable 带返回值求和任务
+ * @create 2026/8/8
+ */
+public class Demo_03_姓名 {
+    // ========== ① 实现 Callable 接口,泛型指定返回类型 ==========
+    static class SumCallable implements Callable<Integer> {
+        private int n;   // 求 1~n 的和
+        public SumCallable(int n) {
+            this.n = n;
+        }
+        @Override
+        public Integer call() throws Exception {
+            // TODO 1: int sum = 0; 计算 1~n 的累加和
+            // TODO 2: 打印 Thread.currentThread().getName() + "求和结果:" + sum
+            // TODO 3: return sum;   // 返回计算结果(get() 会拿到这个值)
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        // TODO 4: 创建 3 个 SumCallable 对象(求 1~100 / 1~1000 / 1~10000 的和)
+        // TODO 5: 用 FutureTask<Integer> 包装每个 Callable 对象
+        // TODO 6: new Thread(futureTask, "线程名") 创建 3 个线程并 start()
+        // TODO 7: 调用 ft.get() 获取每个线程的返回值,打印并在 main 中汇总总结果
+        // TODO 8: (思考)对比三种实现方式:run() 无返回值 vs call() 有返回值,各自适合什么场景
+    }
+}
+```
+
+**预期输出示例**(每次运行顺序可能不同):
+
+```
+线程1求和结果:5050
+线程2求和结果:500500
+线程3求和结果:50005000
+线程1---->5050
+线程2---->500500
+线程3---->50005000
+三个线程求和汇总:505055050
+```
+
+**要求说明**:
+1. 必须实现 `Callable<Integer>`(泛型指定返回类型为 Integer),重写 `call()` 方法并 `return` 计算结果
+2. 必须用 `FutureTask<Integer>` 包装 Callable,再传给 `Thread` 构造(Thread 构造只接收 Runnable/FutureTask)
+3. 必须用 `ft.get()` 获取每个线程的返回值并打印(含 main 汇总)
+4. `get()` 与 `sleep()` 需要处理异常(main 直接 `throws Exception` 即可)
+
+**💡 提示**:
+- `FutureTask` 是"任务容器":它包装 Callable 后交给线程执行;`get()` 会**阻塞等待**该线程计算完成再返回 `call()` 的结果
+- 三个线程可以用**同一个** SumCallable(结果相同),也可以用**三个不同范围**的(更直观地看到不同返回值)
+- `main throws Exception` 可以同时覆盖 `InterruptedException`(sleep)与 `ExecutionException`(get)
+- 对比:继承 Thread / 实现 Runnable 的 `run()` **无返回值**,结果只能在 run() 内部打印;Callable 的 `call()` **有返回值**,结果可以交回主线程用 `get()` 取
+
+**🎯 挑战加分(选做)**:
+1. 在 `call()` 中加 `Thread.sleep(1000)` 模拟耗时计算,观察 `get()` 调用前的 main 是否被"卡住"等待(体会 get 的阻塞特性)
+2. 用 **匿名内部类** / **Lambda** 实现 Callable(不建 SumCallable 类),对比三种书写方式
+3. 画一张"三种实现方式对比"表格(有无返回值 / 能否继承其他类 / 能否直接用 getName / 编程难度),并把结论写在代码注释里
+
+---
+
+## 四、提交要求
+
+- **类名**:`Demo_题号_姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`)
+- **文件头注释**:每个 Java 文件头部必须包含 `@author`(你的姓名)、`@title`(题目名称)、`@create 2026/8/8`
+- **提交内容**:源码文件 + 运行结果截图
+- **必做/选做**:基础题(题目 1)、进阶题(题目 2)为**必做**;挑战题(题目 3)为**选做**,鼓励学有余力的同学完成
+- **运行环境**:JDK 8 及以上(方式三使用 `java.util.concurrent` 并发包,JDK 自带)

+ 1246 - 0
课堂笔记/01-JavaSE基础-笔记/20260808-笔记.md

@@ -0,0 +1,1246 @@
+# 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)](#1-转换流概念字符集编码--解码与乱码成demo01)
+2. [InputStreamReader —— 字节流到字符流的桥梁](#2-inputstreamreader--字节流到字符流的桥梁)
+3. [OutputStreamWriter —— 字符流到字节流的桥梁](#3-outputstreamwriter--字符流到字节流的桥梁)
+4. [转换流综合案例:UTF-8 文件转 GBK 文件(Demo01 main)](#4-转换流综合案例utf-8-文件转-gbk-文件demo01-main)
+5. [对象持久化概念:把内存中的对象保存到磁盘(Demo02)](#5-对象持久化概念把内存中的对象保存到磁盘demo02)
+6. [对象序列化:ObjectOutputStream + writeObject()](#6-对象序列化objectoutputstream--writeobject)
+7. [对象反序列化:ObjectInputStream + readObject()](#7-对象反序列化objectinputstream--readobject)
+8. [Serializable 标识接口与 serialVersionUID 版本兼容(InvalidClassException)](#8-serializable-标识接口与-serialversionuid-版本兼容invalidclassexception)
+9. [transient 关键字:跳过序列化的属性](#9-transient-关键字跳过序列化的属性)
+10. [序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新)](#10-序列化注意事项静态属性引用链构造方法单例demo02-更新)
+11. [IO 流体系总结:基础流 vs 处理流全景(Demo03)](#11-io-流体系总结基础流-vs-处理流全景demo03)
+12. [Student 实体类 —— 可序列化对象综合演示](#12-student-实体类--可序列化对象综合演示)
+13. [序列化 / 反序列化运行演示:ObjectOutputStreamTest / ObjectInputStreamTest](#13-序列化--反序列化运行演示objectoutputstreamtest--objectinputstreamtest)
+14. [课后作业回顾:接口化文件复制效率对比器(homework0807/Demo_03)](#14-课后作业回顾接口化文件复制效率对比器homework0807demo_03)
+15. [多线程入门:进程、线程、并行与并发、main 主线程(Demo04 与 Hello)](#15-多线程入门进程线程并行与并发main-主线程demo04-与-hello)
+16. [实现多线程方式一:继承 Thread 类,重写 run()(MyThread 与 ThreadTest01)](#16-实现多线程方式一继承-thread-类重写-runmythread-与-threadtest01)
+17. [实现多线程方式二:实现 Runnable 接口(RunnableImpl 与 ThreadTest02)](#17-实现多线程方式二实现-runnable-接口runnableimpl-与-threadtest02)
+18. [实现多线程方式三:实现 Callable 接口 + FutureTask(CallableImpl 与 ThreadTest03)](#18-实现多线程方式三实现-callable-接口--futuretaskcallableimpl-与-threadtest03)
+19. [知识点全景总结](#19-知识点全景总结)
+20. [随堂练习要点](#20-随堂练习要点)
+21. [拓展阅读](#21-拓展阅读)
+
+---
+
+## 1. 转换流概念:字符集编码 / 解码与乱码成因(Demo01)
+
+### 概念 —— 为什么要转换流
+
+上一日学习的字符流使用**平台默认字符集**读写文件。但现实中会遇到这种情况:一个文件本身是 **GBK 编码**的,而我们手头只能使用 **UTF-8** 字符集,想要给这个文件追加写入内容——直接写必然乱码。这时就需要**转换流**:把 A 字符集的文件,转换为 B 字符集的文件。
+
+```java
+// 来源:course/Demo01.java(注释部分)
+/*
+    我们讲字符流的时候,说过字符集的概念,
+    不同的字符集虽然都能转换成字节(二进制数)但是,同一个字节,在不同的字符集中表示的含义是不一样的。
+
+    字符--编码-->字节--解码-->字符
+    如果使用字符集A进行编码,使用字符集B进行解码,就会出现字符集乱码。
+*/
+```
+
+### 编码 / 解码模型
+
+```
+字符 --编码--> 字节 --解码--> 字符
+```
+
+> **要点**:
+> - **编码(Encoding)**:字符 → 字节(字符集编码表负责把文字符号转成二进制数)
+> - **解码(Decoding)**:字节 → 字符(把二进制数按编码表还原成文字符号)
+> - **乱码根源**:不同字符集虽然都能把字符转成字节,但**同一个字节在不同字符集中表示的含义不一样**——用字符集 A 编码、用字符集 B 解码,就会得到错误字符(乱码)
+> - **转换流的应用场景**:把 GBK 编码的文件内容转成 UTF-8(或反之),实现不同字符集文件之间的转换
+
+---
+
+## 2. InputStreamReader —— 字节流到字符流的桥梁
+
+### 概念 —— 读取字节并用指定字符集解码为字符
+
+`InputStreamReader` 是**字节流到字符流的桥梁**,父类是 `Reader`。它**读取字节**,并使用**指定的字符集**把它**解码为字符**。使用的字符集可以**按名称指定**,也可以直接使用**平台默认字符集**。
+
+```java
+// 来源: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`。它**读取字符**,使用**指定的字符集**把它**编码为字节**,再交给底层字节流写入。
+
+```java
+// 来源: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` 文件。
+
+```java
+// 来源: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)可以分享给其他人,但**根据类创建的对象**是在程序运行时于**内存中开辟空间**创建的,无法直接分享。通过**对象持久化**,可以把内存中的对象**保存到磁盘**,或者**在网络中传输对象**,从而分享给他人。
+
+```java
+// 来源:course/Demo02.java(注释部分)
+/*
+    我们可以把一个类文件分享给其他人,但是如果是根据某个类创建的对象,想要分享给其他人,就不容易了
+    因为它是程序运行时,在内存中开辟空间创建的对象。
+    但是我们可以通过【对象持久化】的方式,把内存中的对象,保存到磁盘中,分享其他人。
+    对象持久化:就是把对象保存到磁盘、或者在网络中传输对象。
+    这种机制就是使用一个字节序列表示一个对象,这个字节序列包含:
+        对象的类型
+        对象的数据,和对象中存储的属性等等信息。
+    把字节序列写入到文件中后,就相当于永久的保存了一个对象。
+    反过来,可以使用【对象反序列化】的方式再从文件中读取出对象,进行对象重构。
+*/
+```
+
+### 序列化 / 反序列化模型
+
+```
+序列化(保存):对象(内存) --对象序列化流 ObjectOutputStream--> 字节序列 --> 磁盘文件(永久保存)
+反序列化(读取):磁盘文件 --对象反序列化流 ObjectInputStream--> 字节序列 --> 内存中重新构造对象
+```
+
+> **要点**:
+> - **对象持久化**:把内存中的对象保存到磁盘、或在网络中传输对象——本质是把"对象"变成**字节序列**
+> - **字节序列的内容**:包含**对象的类型**、**对象的数据**、**对象中存储的属性**等信息,所以反序列化时能完整还原对象
+> - **序列化**:对象 → 字节序列 → 文件,**永久保存**对象
+> - **反序列化**:从文件读取字节序列 → 在内存中**重构对象**
+> - 类比:序列化像"拍快照存档",反序列化像"读档恢复"
+
+---
+
+## 6. 对象序列化:ObjectOutputStream + writeObject()
+
+### 概念 —— 对象序列化流
+
+进行对象序列化的方式是使用**对象序列化流** `ObjectOutputStream`,它把对象写入到字节输出流中:
+
+```java
+// 来源:course/Demo02.java(注释部分)
+/*
+    进行对象序列化的方式就是使用【对象序列化流】    ObjectOutputStream
+    构造方法:
+    public ObjectOutputStream(OutputStream out) 创建一个写入指定OutputStream的对象序列化流对象
+
+    对象序列化的方法:
+        public final void writeObject(Object obj)   将指定对象obj写入到ObjectOutputStream
+*/
+```
+
+### 前提 —— 必须实现 Serializable 接口
+
+```java
+// 来源:course/Demo02.java(注释部分)
+/*
+    一个对象想要被序列化,必须要实现一个接口 java.io.Serializable
+    Serializable接口没有任何需要重写的方法,也没有任何属性,它就像一个标识,标志着这个类的对象可以被序列化。
+*/
+```
+
+> **要点**:
+> - **构造方法**:`new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt"))` —— 同样先用文件字节输出流,再包上对象流
+> - **核心方法**:`writeObject(Object obj)` 把任意对象写入流
+> - **Serializable 是标记接口(标识接口)**:没有任何抽象方法、没有任何属性,它像一个"通行证标识",标志该类的对象**可以被序列化**——凡是支持序列化的类都必须实现它
+
+---
+
+## 7. 对象反序列化:ObjectInputStream + readObject()
+
+### 概念 —— 对象反序列化流
+
+`ObjectInputStream` 把对象序列化后的文件进行**反序列化**,返回对象:
+
+```java
+// 来源: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)
+
+### 经典场景 —— 类版本变化后反序列化报错
+
+```java
+// 来源: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` 的**底层机制**说明——为什么类结构变化会导致反序列化失败:
+
+```java
+// 来源: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` 关键字修饰,序列化时这个属性**不会参与序列化**:
+
+```java
+// 来源:course/Demo02.java(注释部分)
+/*
+    如果类中有某个属性不行被序列化,可以给这个成员添加transient关键字修饰:
+    private transient Integer id;
+    那么在序列化的时候,这个属性。就不会参与序列化。
+*/
+```
+
+> **要点**:
+> - **transient 修饰成员变量**:`private transient Integer id;` —— 序列化时跳过该属性
+> - **反序列化结果**:被 transient 修饰的属性,反序列化后为**默认值**(引用类型为 null、基本类型为 0/false)
+> - **典型用途**:密码、缓存、与对象状态无关的临时数据等不宜持久化的字段
+
+---
+
+## 10. 序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新)
+
+### 概念 —— 序列化的 6 条使用须知
+
+代码更新后,`Demo02` 补充了**序列化的注意事项**,总结了使用对象序列化时必须注意的 6 条规则:
+
+```java
+// 来源: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 流按"基础流 / 处理流"两大维度完整归类,并汇总了**常用方法、特殊方法、特殊属性、特殊关键字**:
+
+```java
+// 来源: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
+```
+
+### 常用方法 / 特殊方法 / 特殊属性 / 特殊关键字汇总
+
+```java
+// 来源: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 等知识点:
+
+```java
+// 来源: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 序列化(注释保留)
+
+```java
+// 来源: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 反序列化
+
+```java
+// 来源: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 日学过的"接口化设计 + 字节缓冲流"做了一次综合应用,与本日的"扩展流(处理流)"体系呼应。
+
+### ① 复制器接口:统一复制规范
+
+```java
+// 来源:homework0807/Demo_03.java
+interface CopyFile {
+    String copy(String src, String desc) throws IOException;
+}
+```
+
+### ② 普通字节流实现 Copy01:FileInputStream + FileOutputStream + byte[8192] + try-catch-finally
+
+```java
+// 来源: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 包装底层流
+
+```java
+// 来源: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() 耗时对比
+
+```java
+// 来源: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` 新增了**多线程**主题。首先理解三个基础概念:**程序、进程、线程**。
+
+```java
+// 来源:course/Demo04.java(注释部分,本次更新新增)
+/*
+    什么线程?什么叫进程?
+        我们知道,程序本质上就是一种文件,它是存储在硬盘中,程序想要被执行,就需要被加载到内存中执行。
+        在内存中执行的着的程序就是进程。
+        一个程序运行起来后,就成了进程,需要占用CPU、内存、网络、GPU、磁盘等等....
+        一个进程中可能包含多个线程。
+        比如:360安全卫士,同时进行木马查杀、电脑清理、系统修复、优化加速,每使用一个功能,就是启动一个线程。
+*/
+```
+
+| 概念 | 说明 |
+|------|------|
+| **程序** | 本质是存储在**硬盘**中的文件,是一段静态的代码 |
+| **进程** | **在内存中正在执行的程序**;占用 CPU、内存、网络、GPU、磁盘等资源 |
+| **线程** | **进程中的单个顺序控制流**,是一条执行路径;一个进程可能包含多个线程 |
+
+> **要点**:
+> - 一个程序运行起来后就成了**进程**,需要占用各种硬件资源(CPU / 内存 / 网络 / GPU / 磁盘)
+> - 一个**进程**中可能包含**多个线程**——例如 360 安全卫士同时进行木马查杀、电脑清理、系统修复、优化加速,每个功能就是一个线程
+> - **进程三大特性**:**独立性**(能独立运行、系统分配资源和调度的独立单位)、**动态性**(进程本质是程序的一次执行过程,动态产生、动态消亡)、**并发性**(任何进程都可以和其他进程一起并发执行)
+> - **单线程 vs 多线程**:一个进程只有一条路径就是**单线程程序**;有多条路径就是**多线程程序**
+> - **main 主线程**:`main` 方法被称为主方法,也就是 Java 程序的**主线程**(`Hello.java` 中的 `main` 就是主线程的入口)
+
+### 并行 vs 并发
+
+```java
+// 来源:course/Demo04.java(注释部分)
+/*
+    线程的并行和并发
+    并行:同一个时刻,有多个指令在多个CPU上同时执行。比如:饭店后厨,有3个灶台被三位厨师使用炒菜。
+    并发:同一时刻,有多个指令在单个CPU上交替执行。比如:饭店后厨,有3个灶台,被一位厨师炒菜。
+*/
+```
+
+| 概念 | 含义 | 后厨类比 |
+|------|------|----------|
+| **并行(Parallel)** | 同一个时刻,多个指令在**多个 CPU** 上**同时执行** | 3 个灶台被 3 位厨师同时使用炒菜 |
+| **并发(Concurrent)** | 同一时刻,多个指令在**单个 CPU** 上**交替执行** | 3 个灶台被 1 位厨师轮番炒菜 |
+
+> **要点**:
+> - **并行靠硬件**:需要多个 CPU 核心同时工作,是"真·同时"
+> - **并发靠调度**:单核 CPU 在极短时间内快速切换执行多个线程,宏观上"像同时在跑",微观上是交替执行
+> - **多线程的意义**:从软硬件上实现多个线程并发执行,充分利用 CPU,提升性能——这也解释了为什么"多线程 ≠ 一定快"(单核场景下是并发切换)
+
+### 补充 —— CPU 与 GPU 的分工(Demo04 注解)
+
+`Demo04` 还顺带对比了 CPU / GPU 的特性差异,帮助理解"多核 / 多线程"的硬件背景:
+
+```java
+// 来源: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 个步骤:
+
+```java
+// 来源:course/Demo04.java(注释部分)
+/*
+    Java中实现多线程的方式:
+        方式一: 继承 Thread 类,重写 run()方法。
+            实现步骤:
+                1、自定义一个类,继承Thread类
+                2、在自定义类中重写父类的run()方法,设置在线程中要执行的操作。
+                3、创建自定义线程类的对象
+                4、调用自定义线程类的start()方法 启动线程。
+*/
+```
+
+### run() 方法 vs start() 方法的区别
+
+```java
+// 来源: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()` 中封装"求和 + 打印"的操作:
+
+```java
+// 来源: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()` 启动:
+
+```java
+// 来源: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),方式二**更推荐**:
+
+```java
+// 来源: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)
+
+```java
+// 来源: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()` 中打印"熊大熊二光头强"并循环输出当前线程名:
+
+```java
+// 来源: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 条执行路径并发:
+
+```java
+// 来源: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` 使用:
+
+```java
+// 来源: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 的和并返回:
+
+```java
+// 来源: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()` 拿到每个线程的计算结果:
+
+```java
+// 来源: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 更新)
+
+```java
+// 来源: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 更新)
+
+```java
+// 来源: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`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/InputStreamReader.html)、[`java.io.OutputStreamWriter`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/OutputStreamWriter.html)、[`java.io.ObjectOutputStream`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/ObjectOutputStream.html)、[`java.io.ObjectInputStream`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/ObjectInputStream.html)、[`java.io.Serializable`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Serializable.html)
+- **字符集与乱码**:文件的"保存编码"与"读取编码"必须一致;`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`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html)(线程类,`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`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Callable.html)、[`java.util.concurrent.FutureTask`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/FutureTask.html)、[`java.util.concurrent.Future`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html)
+- **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` 等),是生产环境多线程编程的主流工具

+ 360 - 0
课堂练习题/20260808-对象序列化与转换流练习.md

@@ -0,0 +1,360 @@
+# 2026-08-08 对象序列化与反序列化 + 转换流练习(课堂练习用)
+
+> **说明**:练习今日新学知识点——**对象序列化与反序列化(对象持久化)**(`Serializable` 标记接口、`ObjectOutputStream.writeObject()` 序列化 / `ObjectInputStream.readObject()` 反序列化、`serialVersionUID` 版本兼容与 `InvalidClassException`、`transient` 跳过序列化、对象强转)以及**转换流**(`InputStreamReader` / `OutputStreamWriter`:字节 ↔ 字符的桥梁、字符集编码 / 解码、GBK / UTF-8 转码、乱码原理)。
+
+---
+
+## 练习一:多个学生对象序列化与反序列化(对象集合持久化)
+
+**难度**:⭐⭐⭐⭐
+**知识点**:`Serializable`、`ObjectOutputStream`、`ObjectInputStream`、`ArrayList` 集合、`writeObject()` / `readObject()`、对象强转、`FileOutputStream` / `FileInputStream`
+
+**场景描述**:课堂学了**对象持久化**:把内存中的对象用 `ObjectOutputStream` 序列化到磁盘文件,再用 `ObjectInputStream` 反序列化读回内存。但是**一个对象一次写入太麻烦**——本例把**多个 Student 对象装进 ArrayList 集合**,把"整个集合对象"一次性序列化到文件,再一次性反序列化读回。集合在 Java 中也是对象,同样可以序列化。
+
+### 案例需求
+
+创建多个学生类对象写到文件中,再次读取到内存中。
+
+### 实现步骤
+
+1. 创建序列化流对象
+2. 创建多个学生对象
+3. 将学生对象添加到集合中
+4. 将集合对象序列化到文件中
+5. 创建反序列化流对象
+6. 将文件中的对象数据,读取到内存中
+
+### 代码框架
+
+```java
+import java.io.*;
+import java.util.ArrayList;
+
+// 学生类:想要被序列化,必须实现 Serializable 接口
+class Student implements Serializable {
+    private static final long serialVersionUID = 42L;   // 固定序列化版本号
+    private Integer id;
+    private String name;
+    private Integer age;
+
+    public Student() { }
+    public Student(Integer id, String name, Integer age) {
+        this.id = id;
+        this.name = name;
+        this.age = age;
+    }
+    // getter / setter 省略...
+
+    @Override
+    public String toString() {
+        return "Student{id=" + id + ", name='" + name + "', age=" + age + "}";
+    }
+}
+
+public class ObjectListTest {
+    public static void main(String[] args) throws IOException, ClassNotFoundException {
+        // ========== 1、创建序列化流对象 ==========
+        // TODO 1: new ObjectOutputStream(new FileOutputStream("D:/students.txt"))
+
+        // ========== 2、创建多个学生对象 ==========
+        // TODO 2: 创建张三、李四、王五 等 3 个 Student 对象
+
+        // ========== 3、将学生对象添加到集合中 ==========
+        // TODO 3: new ArrayList<Student>(),用 list.add(...) 把 3 个学生加入集合
+
+        // ========== 4、将集合对象序列化到文件中 ==========
+        // TODO 4: oos.writeObject(list);   一次把整个集合写入文件
+        // TODO 5: oos.close();
+
+        // ========== 5、创建反序列化流对象 ==========
+        // TODO 6: new ObjectInputStream(new FileInputStream("D:/students.txt"))
+
+        // ========== 6、将文件中的对象数据,读取到内存中 ==========
+        // TODO 7: Object o = ois.readObject();   读回的是 Object
+        // TODO 8: 强转为 ArrayList<Student> 类型,遍历输出每个学生
+        // TODO 9: ois.close();
+    }
+}
+```
+
+**输出示例**:
+```
+反序列化读取到的学生:
+Student{id=1001, name='张三', age=18}
+Student{id=1002, name='李四', age=19}
+Student{id=1003, name='王五', age=20}
+```
+
+**选做加分**:
+- 给 Student 的 `id` 加 `transient` 修饰再次序列化 / 反序列化,观察读回后 id 是否变成 null
+- 连续两次 `writeObject()` 序列化两个集合,再连续两次 `readObject()` 读回,体会**写入顺序与读取顺序必须一致**
+- 先写入集合再写入一个单独对象,反序列化时按相同顺序分别读取
+
+**思考题**:
+1. 为什么集合 `ArrayList` 也能被序列化?(提示:ArrayList 实现了 Serializable 接口,集合也是对象)
+2. `readObject()` 返回什么类型?为什么需要强转?(提示:返回 Object,强转后才有 Student 的 toString / getter 可用)
+3. 反序列化时为什么 main 要 `throws ClassNotFoundException`?(提示:readObject 需要根据文件里的类信息加载类,找不到就抛异常)
+4. 序列化到文件的内容是什么?直接打开文件看是乱码吗?(提示:是二进制字节序列,不是文本)
+
+---
+
+## 练习二:transient 与 serialVersionUID 验证(类版本兼容实验)
+
+**难度**:⭐⭐⭐⭐
+**知识点**:`transient` 关键字、`serialVersionUID`、`InvalidClassException`、序列化版本兼容
+
+**场景描述**:课堂讲了序列化的两个"坑":① `transient` 修饰的属性**不参与序列化**;② 类结构(属性)变化后反序列化会抛 **`InvalidClassException`**,加上 `serialVersionUID` 固定版本号即可兼容。本例用实验把这两个知识点验证一遍。
+
+### 案例需求
+
+验证 transient 属性能否被序列化,以及 serialVersionUID 对类版本变化的影响。
+
+### 实现步骤
+
+1. 序列化一个带 transient 属性的学生对象到文件
+2. 反序列化读回,观察 transient 属性的值是否为默认值(null)
+3. 修改 Student 类的属性(新增 / 删除字段),去掉 serialVersionUID 再反序列化 → 观察是否报 InvalidClassException
+4. 加上 serialVersionUID 固定版本号,再反序列化 → 验证兼容
+
+### 代码框架
+
+```java
+import java.io.*;
+
+// 学生类:id 用 transient 修饰,不参与序列化
+class Student implements Serializable {
+    // TODO: 练习时先去掉下面这行,修改类结构后观察 InvalidClassException
+    private static final long serialVersionUID = 42L;
+
+    private transient Integer id;    // transient:序列化时跳过该属性
+    private String name;
+    private Integer age;
+    // TODO: 尝试新增一个属性(如 private String sex;)观察反序列化结果
+
+    public Student(Integer id, String name, Integer age) {
+        this.id = id;
+        this.name = name;
+        this.age = age;
+    }
+    @Override
+    public String toString() {
+        return "Student{id=" + id + ", name='" + name + "', age=" + age + "}";
+    }
+}
+
+public class TransientTest {
+    public static void main(String[] args) throws IOException, ClassNotFoundException {
+        // ========== 1、序列化一个学生对象 ==========
+        // TODO 1: 创建 Student 对象(id=1001, name=张三, age=18)
+        // TODO 2: ObjectOutputStream 序列化到 D:/student.txt
+
+        // ========== 2、反序列化读回,观察 transient 属性 ==========
+        // TODO 3: ObjectInputStream 读回,强转后打印
+        //         预期:id=null(transient 不参与序列化,反序列化为默认值)
+    }
+}
+```
+
+**输出示例**:
+```
+反序列化结果:Student{id=null, name='张三', age=18}   ← id 是 transient,变成 null
+```
+
+**选做加分**:
+- 修改 Student 类**新增**一个属性(如 `sex`),**去掉** serialVersionUID 后反序列化 → 观察 `InvalidClassException` 的异常信息
+- 再加上 `serialVersionUID = 42L` 固定版本号,再次反序列化 → 观察不再报错,新属性 sex 值为 null
+- 用 `s instanceof Serializable` 判断一个对象能否被序列化
+
+**思考题**:
+1. `transient` 修饰的属性序列化后变成什么?(提示:反序列化后为默认值——引用类型 null、基本类型 0/false)
+2. 去掉 serialVersionUID 后修改类结构,为什么反序列化会报 `InvalidClassException`?(提示:没有显式版本号时 JVM 自动生成一个,类结构变化 → 自动版本号变化 → 两端不一致)
+3. 加上 serialVersionUID 后类结构变了为什么还能读?(提示:版本号一致即可兼容;有的属性正常赋值,没有的属性给默认值)
+4. 哪些成员天然不参与序列化?(提示:static 修饰的类成员不属于对象,本身就不参与序列化)
+
+---
+
+## 练习三:转换流字符集转码(UTF-8 文件 → GBK 文件)
+
+**难度**:⭐⭐⭐
+**知识点**:`InputStreamReader`、`OutputStreamWriter`、字符集、编码 / 解码、GBK / UTF-8、乱码原理
+
+**场景描述**:课堂学了**转换流**:`InputStreamReader` 是字节流 → 字符流的桥梁(按指定字符集**解码**),`OutputStreamWriter` 是字符流 → 字节流的桥梁(按指定字符集**编码**)。现在把 `D:/abc.txt`(UTF-8 编码)转码为 `D:/hello.txt`(GBK 编码),体会"读端指定源字符集、写端指定目标字符集"的转码流程。
+
+### 案例需求
+
+使用 UTF-8 字符集打开 abc.txt 文件,并将内容转换为 GBK 字符集写入到 hello.txt 文件。
+
+### 实现步骤
+
+1. 创建输入转换流:`InputStreamReader` 包装文件字节输入流,指定 UTF-8
+2. 创建输出转换流:`OutputStreamWriter` 包装文件字节输出流,指定 GBK
+3. 用 char[1024] 循环读取字符并写入
+4. 关闭流,释放资源
+
+### 代码框架
+
+```java
+import java.io.*;
+
+public class ConvertStreamTest {
+    public static void main(String[] args) throws IOException {
+        // ========== 1、输入转换流:按 UTF-8 解码 ==========
+        // TODO 1: new InputStreamReader(new FileInputStream("D:/abc.txt"), "UTF-8")
+
+        // ========== 2、输出转换流:按 GBK 编码 ==========
+        // TODO 2: new OutputStreamWriter(new FileOutputStream("D:/hello.txt"), "GBK")
+
+        // ========== 3、char[1024] 循环读取字符并写入 ==========
+        // TODO 3: char[] chars = new char[1024];
+        //         while ((count = isr.read(chars)) != -1) { osw.write(chars, 0, count); }
+
+        // ========== 4、关闭流,释放资源 ==========
+        // TODO 4: osw.close();  isr.close();
+        System.out.println("转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt");
+    }
+}
+```
+
+**输出示例**:
+```
+转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt
+```
+(用 GBK 编码的记事本打开 `D:/hello.txt`,中文正常显示;文件大小比 UTF-8 文件小——一个汉字 GBK 占 2 字节)
+
+**选做加分**:
+- 把写端字符集改成 UTF-8(读写都是 UTF-8),对比文件字节大小差异,理解"同一中文在不同字符集中占字节数不同"
+- 反向转码:读取 GBK 文件 → 写入 UTF-8 文件,体会"读端指定 GBK、写端指定 UTF-8"
+- 用默认字符集的 `FileReader` 读取转码后的 GBK 文件,观察乱码现象;再改用指定 GBK 的转换流读取,验证乱码消失
+
+**思考题**:
+1. `InputStreamReader` 和 `FileReader` 有什么区别?(提示:FileReader 只能用平台默认字符集,InputStreamReader 可以手动指定字符集)
+2. 转换流为什么属于"扩展流(处理流)"?(提示:它不直接操作文件,而是包装底层 FileInputStream / FileOutputStream 增加转码能力)
+3. 如果读端写 UTF-8、写端也写 UTF-8,会发生什么?(提示:相当于按 UTF-8 拷贝文件内容)
+4. 转码后 GBK 文件的汉字为什么比 UTF-8 的占字节少?(提示:GBK 汉字 2 字节,UTF-8 汉字 3 字节)
+
+---
+
+# 参考答案要点
+
+## 练习一:ObjectListTest(多个学生对象集合序列化)
+
+```java
+// 1、创建序列化流对象(TODO 1)
+ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/students.txt"));
+
+// 2、创建多个学生对象(TODO 2)
+Student s1 = new Student(1001, "张三", 18);
+Student s2 = new Student(1002, "李四", 19);
+Student s3 = new Student(1003, "王五", 20);
+
+// 3、将学生对象添加到集合中(TODO 3)
+ArrayList<Student> list = new ArrayList<>();
+list.add(s1);
+list.add(s2);
+list.add(s3);
+
+// 4、将集合对象序列化到文件中(TODO 4~5)
+oos.writeObject(list);     // 一次写入整个集合(集合也是对象)
+oos.close();
+
+// 5、创建反序列化流对象(TODO 6)
+ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/students.txt"));
+
+// 6、将文件中的对象数据,读取到内存中(TODO 7~9)
+Object o = ois.readObject();                          // 读回的是 Object
+ArrayList<Student> readList = (ArrayList<Student>) o; // 强转为 ArrayList<Student>
+for (Student stu : readList) {
+    System.out.println(stu);                          // 自动调用重写的 toString()
+}
+ois.close();
+```
+
+```java
+// 选做:连续两次序列化 / 反序列化(写入顺序与读取顺序必须一致)
+oos.writeObject(list1);          // 先写集合1
+oos.writeObject(list2);          // 再写集合2
+Object o1 = ois.readObject();    // 先读集合1
+Object o2 = ois.readObject();    // 再读集合2
+```
+
+## 练习二:TransientTest(transient + serialVersionUID 验证)
+
+```java
+// 1、序列化一个学生对象(TODO 1~2)
+Student s = new Student(1001, "张三", 18);
+ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/student.txt"));
+oos.writeObject(s);
+oos.close();
+
+// 2、反序列化读回,观察 transient 属性(TODO 3)
+ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/student.txt"));
+Student readStu = (Student) ois.readObject();
+System.out.println(readStu);   // Student{id=null, name='张三', age=18}  ← id 是 transient
+ois.close();
+```
+
+```java
+// 选做:serialVersionUID 版本兼容实验
+// ① 去掉类中的 serialVersionUID,再往 Student 类新增属性 private String sex;
+// ② 反序列化 → 抛异常:java.io.InvalidClassException(JVM 自动版本号变化,两端不一致)
+// ③ 恢复 serialVersionUID = 42L,再次反序列化 → 不报错,sex 赋默认值 null
+```
+
+## 练习三:ConvertStreamTest(UTF-8 → GBK 转码)
+
+```java
+// 1、输入转换流:按 UTF-8 解码(TODO 1)
+InputStreamReader isr = new InputStreamReader(new FileInputStream("D:/abc.txt"), "UTF-8");
+
+// 2、输出转换流:按 GBK 编码(TODO 2)
+OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:/hello.txt"), "GBK");
+
+// 3、char[1024] 循环读取字符并写入(TODO 3)
+char[] chars = new char[1024];
+int count;
+while ((count = isr.read(chars)) != -1) {
+    osw.write(chars, 0, count);
+}
+
+// 4、关闭流,释放资源(TODO 4)
+osw.close();
+isr.close();
+System.out.println("转码完成!UTF-8 的 abc.txt 已转为 GBK 的 hello.txt");
+```
+
+```java
+// 选做:反向转码(GBK 文件 → UTF-8 文件)
+InputStreamReader isr = new InputStreamReader(new FileInputStream("D:/gbk.txt"), "GBK");
+OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:/utf8.txt"), "UTF-8");
+char[] chars = new char[1024];
+int count;
+while ((count = isr.read(chars)) != -1) {
+    osw.write(chars, 0, count);
+}
+osw.close();
+isr.close();
+```
+
+---
+
+# 涵盖知识点总览
+
+| 知识点 | 说明 |
+|--------|------|
+| `Serializable` 标记接口 | 无方法无属性,实现它即表示该类的对象可以被序列化 |
+| `ObjectOutputStream` | 对象序列化流,`writeObject(Object)` 把对象写入字节输出流 |
+| `ObjectInputStream` | 对象反序列化流,`readObject()` 从字节输入流读回对象(返回 Object 需强转) |
+| `writeObject(list)` | 集合也是对象(ArrayList 实现了 Serializable),可以一次性序列化整个集合 |
+| `readObject()` + 强转 | `(ArrayList<Student>) ois.readObject()` 强转后才能遍历访问 |
+| `serialVersionUID` | 固定类的版本号;类结构变化后反序列化不报 InvalidClassException,缺失属性赋默认值 |
+| `InvalidClassException` | 类结构变化且版本号不一致时,反序列化直接抛异常 |
+| `transient` | 修饰成员变量,序列化时跳过该属性,反序列化为默认值(引用类型 null) |
+| `toString()` 重写 | 打印对象 / 字符串拼接时自动调用,输出可读的对象内容 |
+| 序列化写读顺序 | 写入顺序与读取顺序必须一致(先写先读) |
+| `InputStreamReader` | 字节流 → 字符流桥梁(父类 Reader),按指定字符集**解码**字节为字符 |
+| `OutputStreamWriter` | 字符流 → 字节流桥梁(父类 Writer),按指定字符集**编码**字符为字节 |
+| 转换流构造方法 | `InputStreamReader(InputStream, String charsetName)` / `OutputStreamWriter(OutputStream, String charsetName)` |
+| 编码 / 解码 | 字符--编码-->字节--解码-->字符;A 编码 + B 解码 = 乱码 |
+| GBK / UTF-8 | 一个汉字:GBK 占 2 字节,UTF-8 占 3 字节 |
+| 转码流水线 | UTF-8 文件 → InputStreamReader(UTF-8) → char[] → OutputStreamWriter(GBK) → GBK 文件 |
+| 扩展流(处理流) | 转换流 / 对象流都包装底层字节流,不直接操作文件 |