# 20260812 课堂笔记 — 类加载机制与反射 - **日期**:2026-08-12 - **项目**:`c260812` - **包路径**:`course` - **作者**:WanJL - **内容概览**:类的加载机制(类加载 6 时机 / 加载过程 3 大步 5 小步 / Demo01 静态代码块→代码块→构造方法执行顺序)+ 类加载器与双亲委派(Demo02 三种类加载器 / Demo03 双亲委派模型 + Properties 加载配置文件)+ 反射(Demo04 反射概念 / Demo05 获取 Class 对象三种方式 / Demo06 反射-Constructor 构造方法:获取构造方法 + 暴力反射访问私有构造方法) --- ## 目录 1. [类的加载机制概述:从 .java 到执行的完整流程](#1-类的加载机制概述从-java-到执行的完整流程) 2. [类加载器的作用:把 .class 文件加载到内存](#2-类加载器的作用把-class-文件加载到内存) 3. [类加载的时机:什么时候会加载类](#3-类加载的时机什么时候会加载类) 4. [类的加载过程:加载 → 链接(验证/准备/解析)→ 初始化](#4-类的加载过程加载--链接验证准备解析--初始化) 5. [Demo01 执行顺序演示:静态代码块 → 代码块 → 构造方法](#5-demo01-执行顺序演示静态代码块--代码块--构造方法) 6. [类加载器:三种类加载器与双亲委派(Demo02)](#6-类加载器三种类加载器与双亲委派demo02) 7. [双亲委派模型:为什么需要双亲委派(Demo03)](#7-双亲委派模型为什么需要双亲委派demo03) 8. [用类加载器加载资源配置文件:getResourceAsStream + Properties(Demo03 + prop.properties)](#8-用类加载器加载资源配置文件getresourceasstream--propertiesdemo03--propproperties) 9. [反射机制:运行时动态获取类的信息(Demo04)](#9-反射机制运行时动态获取类的信息demo04) 10. [获取 Class 对象的三种方式(Demo05 + Student)](#10-获取-class-对象的三种方式demo05--student) 11. [反射 Constructor 构造方法:获取与暴力反射(Demo06 + Student)](#11-反射-constructor-构造方法获取与暴力反射demo06--student) 12. [知识点全景总结](#12-知识点全景总结) 13. [随堂练习要点](#13-随堂练习要点) 14. [拓展阅读](#14-拓展阅读) --- ## 1. 类的加载机制概述:从 .java 到执行的完整流程 ### 概念 —— Java 程序是如何被执行的 我们写的 `.java` 源文件并不能直接运行,需要经过 **编译 → 类加载 → JVM 解释 → 执行** 四个阶段。Java 的"一次编写、到处运行"正依赖于这套机制:`.java` 编译成与平台无关的 `.class` 字节码,再由不同平台上的 JVM 解释执行。 ```java // 来源:course/Demo01.java(类注释) /* java文件 -- 编译--> 字节码文件.class文件--解释-->二进制机器码--执行--> 类加载器 类加载器的作用就是将就是把.class文件加载到内存中 java文件 -- 编译--> class文件 --类加载器--> 内存 --> JVM(java虚拟机)--解释--> 二进制机器码--->执行 */ ``` ``` ┌─────────┐ 编译 ┌────────────┐ 类加载器 ┌────────┐ JVM 解释 ┌────────────────┐ │ .java 源文件 │ ─────► │ .class 字节码 │ ──────────► │ 内存 │ ──────────► │ 二进制机器码执行 │ └─────────┘ └────────────┘ └────────┘ └────────────────┘ ``` | 阶段 | 说明 | |------|------| | **编译** | `javac` 把 `.java` 源文件编译成 `.class` 字节码文件(与平台无关) | | **类加载** | 类加载器把 `.class` 文件加载到 JVM 内存中(本课核心) | | **解释执行** | JVM(java 虚拟机)将字节码解释为二进制机器码交给 CPU 执行 | > **要点**: > - **`.class` 字节码是平台无关的**——所以同一份字节码可以在 Windows / Linux / Mac 上运行,这就是 Java 跨平台的原理 > - **类加载器是"搬运工"**——它的任务就是把磁盘上的 `.class` 文件搬到 JVM 内存中 --- ## 2. 类加载器的作用:把 .class 文件加载到内存 ### 概念 —— 谁是"搬运工" **类加载器(ClassLoader)** 的作用非常单一而重要:**把 `.class` 文件加载到内存中**。没有类加载器,字节码永远只是磁盘上的一堆文件,JVM 无法使用它们。 ```java // 来源:course/Demo01.java(类注释) /* 类加载器 类加载器的作用就是将就是把.class文件加载到内存中 加载 的时候,加载的不一定是.class文件,也可能是某个jar包,jar包就是把Java文件/class文件封装成可以执行的程序包,让别人使用 */ ``` ### 加载的不只是 .class 文件,还可能是 jar 包 类加载的输入并不局限于单个 `.class` 文件——**加载的也可能是某个 jar 包**。**jar 包**就是把多个 Java/class 文件封装成的**可执行的程序包**,方便打包分发、被别人引用使用(第三方框架如 MySQL 驱动、Spring 等都是 jar 包形式)。 > **要点**: > - 类加载器处理的输入是 **`.class` 字节码文件(或 jar 包)**,输出是 **内存中的 Class 对象** > - **jar 包** = 把 Java/class 文件封装成**可执行的程序包**,让别人使用(项目的依赖、第三方库都是 jar 包) > - 这个过程发生在"编译之后、解释执行之前" --- ## 3. 类加载的时机:什么时候会加载类 ### 概念 —— 懒加载:用到了才加载 Java 的类加载遵循 **"使用时才加载,不使用就不加载"** 的懒加载原则。只有当你真正用到某个类时,JVM 才会把它加载进内存。类加载的时机(触发点)一共有 **6 种**: ```java // 来源:course/Demo01.java(类注释) /* 类加载的时机(什么时候会加载类): 1、创建类的实例对象的时候 2、调用类的static方法的时候 3、访问类或接口的static变量或者为该变量赋值的时候。 4、使用反射的方式强制创建某个类或接口对应的java.lang.Class类型的对象的时候。 5、初始化某个类的字类对象的时候 6、直接使用Java.exe命令运行某个主类的时候。 使用这个类的时候才会加载这个类,不使用的时候就不加载。 */ ``` | 触发场景 | 示例代码 | |----------|----------| | **① 创建类的实例对象** | `new Demo01()` | | **② 调用类的 static 方法** | `Math.abs(-1)`、`Arrays.sort(arr)` | | **③ 访问 / 赋值 static 变量** | `System.out`、`Student.schoolName = "爱扣钉"` | | **④ 反射创建 Class 对象** | `Class.forName("course.Demo01")` | | **⑤ 初始化某个类的子类** | `new SubStudent()` 时先加载父类 `Student` | | **⑥ Java.exe 运行主类** | `java course.Demo01`(含 main 方法的类) | > **要点**: > - **懒加载原则**:使用这个类的时候才会加载,不使用就不加载——节省内存、提升启动速度 > - **只要触发以上任意一种场景**,JVM 就会启动类加载流程 > - 注意第 ⑤ 条:**初始化子类会先触发父类的加载**(先有父后有子,加载顺序从父到子) --- ## 4. 类的加载过程:加载 → 链接(验证/准备/解析)→ 初始化 ### 概念 —— 3 大步 5 小步 类的加载过程大体分为 **3 步**(也可细分为 **5 步**): - **① 加载(Loading)** - **② 链接(Linking)** —— 由 3 个小子步组成: - 验证(Verify) - 准备(Prepare) - 解析(Resolve) - **③ 初始化(Initialization)** ```java // 来源:course/Demo01.java(类注释) /* 类的加载过程大体分为3步,也有分为5步: 1、加载 2、链接 |- 验证 |- 准备 |- 解析 3、初始化 1、加载 通过包名+类名(全限定名)获取这个类,准备用流进行传输 把这个类加载到内存中 加载完毕后创建一个Class对象 2、链接 2.1 验证 确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。 说白了就是看文件的信息是否有安全隐患,是否符合虚拟机的规范 2.2 准备 负责为类的类变量(static修饰的变量)分配内存,并且设置默认初始化值(初始化静态变量) 2.3 解析 将类的二进制数据流中的[符号引用]替换为[直接引用],如果本类中使用了其他的类型,那么在这一步就会开始找了,即需要找到使用的那个类。 没有就报错。 3、初始化 根据我们通过程序指定的主观计划去初始化变量和其他资源(给静态变量赋值以及初始化其他资源) */ ``` ### 三步五小步结构图 ``` 类的加载过程 ├── 1. 加载(Loading) │ 通过【全限定名】(包名.类名,如 java.lang.Object) 获取二进制字节流 │ → 将字节流代表的静态存储结构转化为运行时数据结构 │ → 在内存中生成代表这个类的 java.lang.Class 对象 ├── 2. 链接(Linking) │ ├── 2.1 验证(Verify):检查字节流信息是否符合虚拟机规范、有无安全隐患 │ ├── 2.2 准备(Prepare):为 static 类变量分配内存并设置默认初始化值(如 int→0) │ └── 2.3 解析(Resolve):把符号引用替换为直接引用;本类用到其他类型时在此查找,找不到就报错 └── 3. 初始化(Initialization) 根据程序指定的主观计划初始化变量和其他资源(给静态变量赋值等) ``` ### 每步细节说明 | 步骤 | 干什么 | 关键点 | |------|--------|--------| | **① 加载** | 通过**全限定名**(包名.类名)找到 `.class` 文件,用流读取字节流 → 转为运行时数据结构 → 在堆中生成 `java.lang.Class` 对象 | 任何类被使用时,系统都会为它建立一个 Class 对象 | | **②-1 验证** | 检查字节流是否符合 JVM 规范、**不会危害虚拟机自身安全** | 防止加载恶意/损坏的字节码 | | **②-2 准备** | 为 **static 类变量**分配内存并设置**默认值**(如 int→0、引用→null) | 注意:此时还没执行我们写的赋值语句,只是默认初始化 | | **②-3 解析** | 将字节流中的**符号引用替换为直接引用**;如果本类使用了其他类型,在这一步开始查找 | 找不到使用到的类就**报错** | | **③ 初始化** | 按程序指定的主观计划执行初始化(给静态变量赋值、执行静态代码块等) | 真正执行 `static int a = 10`、`static {...}` 等代码 | > **要点**: > - **加载** = 把字节码文件变为内存中的 `Class` 对象(生成"类的模具") > - **验证** = 安全检查(有没有安全隐患、是否符合规范) > - **准备** = 给 static 变量"占位"分配内存并设**默认值**(此时还不是我们写的值) > - **解析** = 把"符号引用"(类的名字)换成"直接引用"(内存里的实际地址),用到别的类就在这步去找 > - **初始化** = 真正执行静态变量赋值语句和静态代码块(我们写的初始化代码在这里跑) > - **小结一句**:加载"把文件搬进来"、验证"检查安全"、准备"先分个默认内存"、解析"找到引用的类"、初始化"跑我们写的初始化代码" --- ## 5. Demo01 执行顺序演示:静态代码块 → 代码块 → 构造方法 ### 概念 —— 类加载阶段与实例化阶段的直观验证 Demo01 用**静态变量 + 静态代码块 + 普通代码块 + 构造方法**同时给 `a` 赋值并打印,可以直观看到**类的加载(静态初始化)发生在实例化之前**,以及类加载"初始化"阶段到底执行了什么: ```java // 来源:course/Demo01.java public class Demo01 { private static int a; // 类变量(static),在"准备"阶段分配内存并设默认值 0 private String str; // 实例变量 private Object obj; public Demo01() { // 构造方法:实例化阶段执行(第 3 个执行) a = 10; System.out.println(a); System.out.println("构造方法..."); str = new String("hello"); obj = new Object(); } { // 普通代码块(构造代码块):实例化阶段执行(第 2 个执行) a = 100; System.out.println(a); System.out.println("代码块...."); } static { // 静态代码块:类加载"初始化"阶段执行(第 1 个执行) a = 10000; System.out.println(a); System.out.println("静态代码块"); } public static void main(String[] args) { Demo01 demo01; demo01 = new Demo01(); // 第一次使用 Demo01 → 触发类加载 → 再实例化 System.out.println(demo01); } } ``` ### 预期输出与执行顺序 ``` 10000 静态代码块 100 代码块.... 10 构造方法... course.Demo01@15db9742 (对象地址,每次运行不同) ``` | 执行顺序 | 打印内容 | 所处阶段 | 说明 | |----------|----------|----------|------| | 第 1 步 | `10000` / `静态代码块` | **类加载的"初始化"阶段** | main 中 `new Demo01()` 第一次使用该类 → 触发类加载 → 初始化阶段执行静态代码块,a = 10000 | | 第 2 步 | `100` / `代码块....` | **实例化(创建对象)** | `new` 创建对象时,先执行普通代码块(构造代码块),a = 100 | | 第 3 步 | `10` / `构造方法...` | **实例化(构造方法)** | 然后执行构造方法,a = 10;同时给 str、obj 赋值 | | 第 4 步 | `course.Demo01@...` | main 收尾 | 打印对象(默认 Object 的 toString) | > **要点**: > - **静态代码块只执行一次**,发生在**类加载的"初始化"阶段**——先于任何对象创建 > - **普通代码块 + 构造方法**属于**实例化阶段**——每 `new` 一次都会执行 > - 三者执行顺序:**静态代码块 → 普通代码块 → 构造方法**(类加载先于实例化) > - **static 变量在"准备"阶段已分配内存并置默认值 0**,初始化阶段再被赋成我们写的值(10000)——印证第 4 节"准备 vs 初始化"的区别 --- ## 6. 类加载器:三种类加载器与双亲委派(Demo02) ### 概念 —— 三个"搬运工"和它们的分工 类加载时需要使用**类加载器(ClassLoader)**。JVM 内置了三种类加载器,分工明确、层级分明: ```java // 来源:course/Demo02.java(类注释) /* 类在加载的时候,是需要使用类加载器进行加载的,大概的分类有三种: BootstrapClassLoader:虚拟机内置的类加载器,一般表示为null,并且没有父null。 PlatformClassLoader:平台类加载,负责加载JDK中一些特殊的模块 SystemClassLoader:系统类加载器,负责加载用户类路径上所指定的类库。 SystemClassLoader 继承 PlatformClassLoader 继承 BootstrapClassLoader */ ``` | 类加载器 | 职责 | 父加载器 | |----------|------|----------| | **BootstrapClassLoader**(启动类加载器) | 虚拟机内置类加载器,加载最核心的 JDK 类(如 `java.lang.Object`) | **无**,一般表示为 `null` | | **PlatformClassLoader**(平台类加载器) | 加载 JDK 中一些特殊的模块 | BootstrapClassLoader | | **SystemClassLoader**(系统/应用类加载器) | 加载**用户类路径(classpath)** 上所指定的类库(我们的代码) | PlatformClassLoader | ``` SystemClassLoader(系统类加载器,加载用户类路径的类) ↑ 父加载器 PlatformClassLoader(平台类加载器,加载 JDK 特殊模块) ↑ 父加载器 BootstrapClassLoader(启动类加载器,虚拟机内置,表示为 null,无父加载器) ``` > **要点**: > - **三层结构**:系统 → 平台 → 启动,上一层是下一层的"父加载器"(**双亲委派模型**) > - **我们的类由 SystemClassLoader 加载**——用户在 classpath 上写的类都由它负责 > - **BootstrapClassLoader 一般表示为 null**(不是 Java 类,是 JVM 内置的),**它没有父加载器** > - 代码里写的是"继承",本质是**双亲委派**:子加载器收到加载请求,先交给父加载器去加载 ### Demo02 代码演示:获取三种类加载器 ```java // 来源:course/Demo02.java public class Demo02 { public static void main(String[] args) { //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); //获取系统类加载器的父类加载器 平台类加载器 ClassLoader platformClassLoader = systemClassLoader.getParent(); //获取平台类加载器的父类加载器 虚拟机类加载器 ClassLoader bootstrapClassLoader = platformClassLoader.getParent(); System.out.println("系统类加载器:" + systemClassLoader); System.out.println("平台类加载器:" + platformClassLoader); System.out.println("虚拟机类加载器:" + bootstrapClassLoader); //也是使用C/C++实现的,所以Java调用输出返回的就是null //虚拟机类加载器 没有父类加载器,而且不为null,直接就是空指针异常 System.out.println("虚拟机类加载器还有没有父类加载器?" + bootstrapClassLoader.getParent()); } } ``` ### 预期输出与运行要点 ``` 系统类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@... 平台类加载器:jdk.internal.loader.ClassLoaders$PlatformClassLoader@... 虚拟机类加载器:null 虚拟机类加载器还有没有父类加载器? ← 到这里抛 NullPointerException(空指针异常) ``` | 代码 | 输出/结果 | 说明 | |------|-----------|------| | `ClassLoader.getSystemClassLoader()` | `AppClassLoader@...` | 拿到**系统类加载器**(JDK 中类名为 AppClassLoader) | | `systemClassLoader.getParent()` | `PlatformClassLoader@...` | 拿到**平台类加载器**——系统类加载器的父加载器 | | `platformClassLoader.getParent()` | `null` | 拿到**启动类加载器**——但它在 Java 中用 `null` 表示 | | `bootstrapClassLoader.getParent()` | **抛空指针异常** | 因为 bootstrapClassLoader 是 `null`,再调 `getParent()` 就空指针了 | > **要点**: > - **获取方式**:`ClassLoader.getSystemClassLoader()` 拿到系统类加载器,再逐级 `getParent()` 向上取父加载器 > - **BootstrapClassLoader 表示为 null**:所以打印"虚拟机类加载器"输出是 `null` > - **BootstrapClassLoader 由 C/C++ 实现**:它本身不是 Java 类,而是 JVM 的一部分,因此 Java 代码调用它时输出返回的就是 `null`(代码注释中已标注) > - **null 上没有方法**:`bootstrapClassLoader.getParent()` 对一个 null 调用方法 → **空指针异常**(运行到这一行程序报错) > - **JDK 中类名**:系统类加载器的类名是 `jdk.internal.loader.ClassLoaders$AppClassLoader`,平台类加载器是 `...$PlatformClassLoader` --- ## 7. 双亲委派模型:为什么需要双亲委派(Demo03) ### 概念 —— 收到加载请求先"向上委派",顶层不行才"自己加载" **双亲委派模型**是三种类加载器之间协同工作的核心机制:**一个类加载器收到类加载请求时,并不会自己先去加载,而是把请求委派给父类加载器执行;如果父类加载器还有父加载器,就继续向上委派,依次递归,直到最顶层的类加载器**。若顶层类加载器能完成加载任务,就成功返回;若顶层无法完成(找不到该类),子类加载器才会尝试自己去加载。 ```java // 来源:course/Demo03.java(类注释) /* 如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把这个请求委派给父类加载器去执行,如果父类加载器还存在父类加载器就会进一步委派, 依次递归,请求最终会委派到最顶层的类加载器,如果顶层的类加载器能过完成鸡杂任务,就成功返回。如果无法完成,子类加载器才会尝试自己去加载,这种模式就是双亲委派模型。 getSystemClassLoader() 获取系统类加载器 getResourceAsStream() 加载某一个资源文件 */ ``` ### 双亲委派执行流程 ``` 加载请求(如加载 course.Demo01) ↓ SystemClassLoader(系统类加载器)—— 不自己先加载,先委派给父加载器 ↓ PlatformClassLoader(平台类加载器)—— 继续委派给父加载器 ↓ BootstrapClassLoader(启动类加载器,最顶层)—— 尝试加载 │ ├── 能加载(如 java.lang.String 核心类)→ 加载成功,直接返回 └── 不能加载(如用户写的 course.Demo01)→ 向下回退 ↓ PlatformClassLoader 尝试加载 → 不能加载 → 继续向下回退 ↓ SystemClassLoader 尝试加载 → 能加载(用户 classpath 上的类)→ 加载成功 ``` > **要点**: > - **委派方向是"向上"**:子加载器收到请求先交父加载器,父再交祖父……一路到最顶层的 BootstrapClassLoader > - **回退方向是"向下"**:顶层加载不了,才逐级往下回退,由子加载器尝试自己加载 > - **核心类由顶层加载**:`java.lang.String` 等核心类只能由最顶层的 BootstrapClassLoader 加载 > - **我们的类由底层加载**:`course.Demo01` 等用户类最终由 SystemClassLoader(系统类加载器)加载 ### 为什么需要双亲委派模型?—— 三个原因 ```java // 来源:course/Demo03.java(类注释) /* 为什么需要双亲委派模型? 避免类被重复加载:同一个类在整个JVM中只有一个Class对象(类的唯一性是由 类+类加载器 共同决定的),双亲委派机制可以保证任意一个类都只被加载一次。 保证核心类库的安全:防止用户编写一个假的 核心类 混入到系统中,双亲委派机制会依次把请求交给最顶层的类加载器, 比如 java.lang.String类只能由顶层的启动类加载器进行加载,用户自定义的同名类永远不会被加载。 保证类的唯一性: 即使我们自定义了类加载器,加载了 自定义的 java.lang.String类,但是由于双亲委派机制,实际上加载的还是最顶层的类加载器加载的核心类, 避免了出现多份不同的String类型的导致的类型混乱。 */ ``` | 原因 | 说明 | 类比 | |------|------|------| | **① 避免类被重复加载** | 同一个类在整个 JVM 中**只有一个 Class 对象**(类的唯一性由 **类 + 类加载器** 共同决定);双亲委派保证任意一个类**只被加载一次** | 全公司只有"一份员工档案",不允许每人各抄一份 | | **② 保证核心类库的安全** | 防止用户编写一个**假的"核心类"**混入系统;双亲委派把请求依次交给最顶层加载器,`java.lang.String` 只能由启动类加载器加载,**用户自定义的同名类永远不会被加载** | 核心系统只有最高层才能签发,防止伪造 | | **③ 保证类的唯一性** | 即使自定义类加载器加载了自定义的 `java.lang.String`,由于双亲委派,实际加载的还是顶层加载器加载的核心类——避免出现**多份不同的 String 类型导致类型混乱** | 全 JVM 只认"官方版"String,不会出现多个版本混淆 | > **要点**: > - **类的唯一性 = 类 + 类加载器共同决定**——同一个类名由不同加载器加载会产生不同的 Class 对象,双亲委派从机制上避免了这种情况 > - **核心类库安全**是双亲委派最重要的价值:**任何人无法通过自定义同名类替换/伪造 JDK 核心类**(如 java.lang.String) > - **一个例子记牢**:自己写一个 `java.lang.String` 类,双亲委派机制下它**永远不会被加载**,JVM 用的始终是启动类加载器加载的官方 String --- ## 8. 用类加载器加载资源配置文件:getResourceAsStream + Properties(Demo03 + prop.properties) ### 概念 —— 类加载器的实际应用:读取配置文件 类加载器除了加载字节码外,还有一个经典的实际应用——**用类加载器加载资源配置文件**。Demo03 演示了如何通过系统类加载器加载根目录下的 `prop.properties` 配置文件,再用 `Properties` 解析其中的数据库连接配置。 ```java // 来源:course/Demo03.java public class Demo03 { public static void main(String[] args) throws IOException { // --- 下面代码的过程 ,其实是编写代码加载资源配置文件的过程 //获取系统类加载器 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); //利用类加载器加载一个指定文件 //参数:文件的路径 //返回值:字节流 InputStream inputStream = classLoader.getResourceAsStream("prop.properties"); //创建解析properties文件的对象 Properties-它的父类是Hashtable 是Map集合接口的另一个实现类 底层是数组 Properties pro = new Properties(); //加载输入流对象 -- 其实就是加载我们指定的文件 pro.load(inputStream); //获取指定的配置信息 String driver = pro.getProperty("db.driver"); String url = pro.getProperty("db.url"); String username = pro.getProperty("db.username"); String password = pro.getProperty("db.password"); //调用连接数据库的方法,执行连接数据库的操作.... 链接数据库的方法(driver, url, username, password); //输出 System.out.println(pro); //关闭流 inputStream.close(); // xml / json / yaml } public static void 链接数据库的方法(String driver, String url, String username, String password) { } } ``` ### 配置文件 prop.properties ```properties # 来源:src/prop.properties # 教学演示示例:以下配置均为占位符,不包含任何真实账号密码信息 db.driver=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/your_database db.username=your_username db.password=******** ``` ### 代码拆解:加载配置文件的全流程 | 代码 | 作用 | 说明 | |------|------|------| | `ClassLoader.getSystemClassLoader()` | 获取**系统类加载器** | 第 6 节 Demo02 已学过的方法 | | `classLoader.getResourceAsStream("prop.properties")` | **加载指定资源文件** | 参数是文件路径(相对于 classpath 根目录),返回**字节流 InputStream** | | `new Properties()` | 创建解析 properties 文件的对象 | **Properties** 的父类是 **Hashtable**,是 **Map 集合接口的另一个实现类,底层是数组** | | `pro.load(inputStream)` | **加载输入流对象** | 把输入流(即我们指定的文件)解析进 Properties | | `pro.getProperty("db.driver")` 等 | **获取指定的配置信息** | 按 key 读取 value,拿到数据库驱动 / 连接地址 / 用户名 / 密码 | | `链接数据库的方法(...)` | 调用连接数据库的方法 | 把读到的 4 个配置传参,执行连接数据库的操作(方法体留空为示例) | | `System.out.println(pro)` | 输出 Properties 对象 | 直观看到加载到的全部配置 | | `inputStream.close()` | **关闭流** | 资源释放(IO 流使用后必须关闭,回顾 08-06/08-07 IO 流知识) | ### 加载配置文件的意义 ``` 配置文件 prop.properties(数据库连接信息:driver/url/username/password) ↓ 类加载器加载(getResourceAsStream 返回字节流) ↓ Properties.load 解析(Properties 底层是 Map/Hashtable,key=value) ↓ getProperty 按 key 取值 数据库连接参数(driver、url、username、password) ↓ 调用连接数据库的方法(执行业务) ``` > **要点**: > - **配置与代码分离**:数据库连接信息写在 `.properties` 配置文件里,代码通过类加载器读取——改配置不用改代码、重新编译,这是企业开发的常规做法 > - **getResourceAsStream 的路径**:参数是**类路径(classpath)根目录下**的文件路径,所以 `prop.properties` 放在 `src/` 根目录即可被加载 > - **Properties**:父类是 **Hashtable**,是 **Map 接口的另一个实现类**(底层是数组)——键值对形式存储,`getProperty(key)` 按 key 取 value > - **IO 流要关闭**:`getResourceAsStream` 返回的 InputStream 用完要 `close()`(衔接 08-06/08-07 的 IO 流知识) > - **常见的配置文件格式**:除了 `.properties`,还有 **xml / json / yaml**——不同格式对应不同的解析方式(本课先掌握 properties) > - **本项目应用场景**:数据库连接四要素(驱动 driver / 连接地址 url / 用户名 username / 密码 password)正是 JDBC 连接数据库的标配参数,后续学习 JDBC 时直接用 --- ## 9. 反射机制:运行时动态获取类的信息(Demo04) ### 概念 —— 什么是反射 **反射(Reflection)** 是 Java 语言提供的一种 **运行时动态机制**,它允许程序在 **运行的状态** 中: - 对 **任意一个类**,都能获取这个类的所有属性和方法(**包括私有的成员 private**) - 对 **任意一个对象**,都能够调用它的任意属性和方法 这种 **动态获取类的信息 + 动态调用对象方法** 的功能,就是 Java 的**反射机制**。 ```java // 来源:course/Demo04.java(类注释) /* 反射 是Java语言提供的一种【运行时动态机制】,他允许程序在【运行的状态】中: 对 任意一个类 都能获取这个类的所有属性和方法(包括私有的成员private) 对 任意一个对象 都能够调用它的任意属性和方法。 这种 动态获取类的信息 + 动态调用对象方法 的功能 就是Java的反射机制。 */ ``` > **一句话总结**:普通方式是代码写死的,反射方式是动态的。反射是 Java 中各种框架的基础——**Spring、MyBatis、Hibernate、JDBC 驱动加载**等等都离不开反射。 ### 普通调用 vs 反射调用:5 维对比 ```java // 来源:course/Demo04.java(类注释) /* 普通的调用 反射调用 的区别: 获取类信息的时机: 普通调用(编译期)- 就是在编译器写死,代码写死。 反射调用(运行期)- 运行时才会动态获取 依赖关系: 普通调用(编译期)- 编译期强耦合,耦合度高 反射调用(运行期)- 运行时才管理,解耦 创建对象: 普通调用(编译期)- new 类名() 反射调用(运行期)- Class.forname() + newInstance() 调用方法: 普通调用(编译期)- 对象.方法名() 反射调用(运行期)- method对象.invoke(对象,参数) 灵活性方面: 普通调用(编译期)- 固定不变 反射调用(运行期)- 高度灵活,可以动态切换 */ ``` | 对比维度 | 普通调用(编译期) | 反射调用(运行期) | |----------|-------------------|--------------------| | **获取类信息的时机** | 在编译期写死,代码写死 | **运行时**才会动态获取 | | **依赖关系** | 编译期强耦合,耦合度高 | 运行时才管理,**解耦** | | **创建对象** | `new 类名()` | `Class.forName()` + `newInstance()` | | **调用方法** | `对象.方法名()` | `method 对象.invoke(对象, 参数)` | | **灵活性** | 固定不变 | 高度灵活,可以**动态切换** | ### 为什么需要反射?—— 三个真实场景 ```java // 来源:course/Demo04.java(类注释) /* 为什么需要反射? 如果没有反射,我们的程序需要使用某个类,就必须在编译期就要知道它的名字,然后使用new创建对象,使用. 调用方法。 这些在很多真实的场景中无法满足需求: 1、配置驱动:比如某个框架需要读取配置文件,文件里面写的时候字符串类型的类名,框架在运行的时候才会拿到这个字符串, 并且需要通过反射才能够把它翻译成真实的类并且进行实例化。 2、通用工具:写一个能够打印任意对象的全部属性的通用工具,调用方法后传入什么参数不确定,那么就只能通过反射来实现。 3、第三方类库的解耦:比如我们使用某个类库,Java只需要定义这个类库的规则(接口),类库自己实现具体的操作, 然后通过反射,在运行时动态加载,源码里面根本就不需要依赖某个具体的类。 */ ``` | 场景 | 说明 | 示例 | |------|------|------| | **① 配置驱动** | 配置文件里写的是**字符串类型的类名**,框架在运行时才拿到这个字符串,需要通过反射才能把它翻译成真实的类并实例化 | 框架读取配置文件 → `Class.forName("com.xxx.Driver")` | | **② 通用工具** | 写一个能**打印任意对象全部属性**的通用工具,调用方法后传入什么参数不确定,只能通过反射实现 | `printUtil(Object obj)` 打印任意对象的所有属性名 | | **③ 第三方类库解耦** | 使用某个类库时,Java 只需要定义这个类库的规则(接口),类库自己实现具体操作,然后通过反射在运行时动态加载——源码里**根本不需要依赖某个具体的类** | 定义接口 + 运行时反射加载实现类 | ### 注意事项 —— 日常业务尽量别用反射 ```java // 来源:course/Demo04.java(类注释) /* 注意:我们需要学习反射,目的是1、学习框架的时候帮助我们理解框架的底层源码。2、当我们进行架构设计、开发中间件程序的时候需要使用反射做解耦和通用适配。 至于日常的业务开发中,一定一定一定尽量不要使用反射。开发使用反射,就相当于玩游戏使用金手指(开挂) 1、使用反射可能破坏封装性 2、使用反射的效率要远比正常创建对象效率低的多,最低3~100倍 */ ``` | 注意点 | 说明 | |--------|------| | **学习反射的目的** | ① 学习框架时帮助理解框架的底层源码;② 架构设计、开发中间件程序时用反射做**解耦和通用适配** | | **日常业务开发** | **一定一定一定尽量不要使用反射**——开发用反射就像玩游戏开挂(金手指) | | **缺点 ① 破坏封装性** | 反射可以获取并调用**私有成员(private)**,破坏类的封装性 | | **缺点 ② 性能低** | 反射的效率远比正常创建对象**低得多,最低 3~100 倍** | ### 反射的工作机制 —— 一切从 Class 对象出发 ```java // 来源:course/Demo04.java(类注释) /* 反射的工作机制: Java程序运行的时候,.class字节码文件会被类加载器(ClassLoader)加载进JVM(Java虚拟机),JVM会为每一个类创建 唯一的一个 java.lang.Class 对象。这个对象就是 "类的类模板",里面记录类这个类的全部信息(类名、父类、接口、字段属性、方法、构造方法等等) 反射的一切操作,都是从这个Class对象出发的。拿到了Class之后,可以依次获取它的构造方法对象(Constructor)、方法对象(Method)、属性对象(Field) 再通过这些对象去创建这个类的实例对象,读写字段属性,调用方法... Person.java源码 --javac编译--> Person.class字节码 --类加载器(ClassLoader)加载-->Person的Class对象(类的类模板) 通过Person的Class对象 |-获取Person类的 构造方法对象Constructor --> newInstance()创建Person对象 |-获取Person类的 字段属性对象Field -->get/set 读写字段属性变量 |-获取Person类的 方法对象Method -->invoke()执行方法 */ ``` ``` Person.java 源码 ↓ javac 编译 Person.class 字节码 ↓ 类加载器(ClassLoader) 加载 Person 的 Class 对象("类的类模板":记录类名/父类/接口/字段/方法/构造方法) ├── 获取构造方法对象 Constructor → newInstance() 创建 Person 对象 ├── 获取字段属性对象 Field → get/set 读写字段属性变量 └── 获取方法对象 Method → invoke() 执行方法 ``` > **要点**: > - **JVM 为每一个类创建唯一的一个 `java.lang.Class` 对象**——它就是"类的类模板",记录该类的全部信息(类名、父类、接口、字段属性、方法、构造方法) > - **反射的一切操作都从这个 Class 对象出发**:拿到 Class 后,依次获取它的构造方法对象(Constructor)、方法对象(Method)、属性对象(Field) > - 再通过 **Constructor/Method/Field** 这些对象去创建实例对象、读写字段属性、调用方法 ### 反射涉及到的核心类 | 核心类 | 所属包 | 作用 | |--------|--------|------| | **Class** | `java.lang.Class` | 表示一个类的模板,是**反射的入口** | | **Constructor** | `java.lang.reflect.Constructor` | 表示一个**构造方法**,可以创建对象 | | **Field** | `java.lang.reflect.Field` | 表示一个**成员变量(字段属性)**,可以读写字段属性 | | **Method** | `java.lang.reflect.Method` | 表示一个**成员方法**,可以调用方法 | | **Modifier** | `java.lang.reflect.Modifier` | 解析修饰符(public / private / static 等等) | | **Parameter** | `java.lang.reflect.Parameter` | 代表方法的参数 | ### Demo04 代码演示:反射的应用场景入口 ```java // 来源:course/Demo04.java package course; import java.lang.reflect.Constructor; /** * @author WanJl * @version 1.0 * @title Demo04 * @description 反射 * @create 2026/8/12 */ public class Demo04 { /* ……(上述全部反射理论知识写在类注释中)…… */ public void printUtil(Object obj){ //输出这个类的所有属性名---就必须要使用反射来实现 } } ``` > **要点**: > - `printUtil(Object obj)` 是一个"通用打印工具"的雏形——**输出任意传入对象的全部属性名,就必须使用反射来实现**(对应"为什么需要反射"场景②通用工具) > - 反射的核心 API(`Class.forName()`、`getDeclaredFields()`、`Field.getName()` 等)会在后续课程深入,本课先建立**反射是什么、为什么需要**的整体认知 > - Demo04 开头的 `import java.lang.reflect.Constructor;` 引入了反射核心类包 `java.lang.reflect` --- ## 10. 获取 Class 对象的三种方式(Demo05 + Student) ### 概念 —— 殊途同归:三种方式拿到同一个 Class 对象 获取 Class 对象的方式一共有 **三种**,它们**殊途同归**:**同一个类无论通过哪种方式,得到的都是同一个对象**——因为 JVM 对每个类**只加载一份字节码**。 ```java // 来源:course/Demo05.java(类注释) /* 获取Class对象的方式有三种,但是殊途同归,同一个类无论通过哪种方式,得到的都是同一个对象,JVM对每个类只加载一份字节码。 方式一: 类名.class 直接调用类名.class变量,这种方式最直接最简单,编译期就会进行类型检查,不允许运行时才失败。前提就是需要在编译期就知道类名。 Class objectClass = Object.class; //JVM需要在编译期就知道这个类是存在的,才行。 Class stringClass = String.class; 方式二: 对象.getClass() getClass()是Object类中定义的方法,所有的对象都能调用,这种方式需要先有一个对象实例,适合在已经拿到对象想要知道它是什么类型的时候使用。 Animal animal=new Teacher(); //父类的引用指向子类的对象--多态 Class aClass = animal.getClass(); 方式三: Class.forName("全类名") Class.forName("全类名")是Class类提供的静态方法,参数是字符串形式的全类名(包名+类名),它是三种方式中最灵活的一种, 类名以字符串的形式传入,可以是变量、配置文件的内容、网络传过来的数据都可以。 所以它也是框架最常用的方式。注意:全类名写错了或类不存在的时候或抛出ClassNotFoundException异常 受检异常必须要处理的。 Class.forName("com.wanjl.Student"); */ ``` | 方式 | 写法 | 特点 | |------|------|------| | **方式一:类名.class** | `Student.class` | 最直接最简单;**编译期就会进行类型检查**,不允许运行时才失败;前提是编译期就知道类名 | | **方式二:对象.getClass()** | `student.getClass()` | `getClass()` 是 **Object 类中定义的方法**,所有对象都能调用;需要**先有一个对象实例**,适合"已经拿到对象想知道它是什么类型"的场景 | | **方式三:Class.forName("全类名")** | `Class.forName("course.Student")` | Class 类提供的**静态方法**,参数是字符串形式的**全类名(包名+类名)**;三种方式中**最灵活**,类名可以是变量、配置文件内容、网络传过来的数据——**框架最常用的方式** | > **要点**: > - **方式一(类名.class)**:编译期类型检查、最安全,但要求编译期就知道类名 > - **方式二(对象.getClass())**:需要先有对象实例,适合运行时判断对象的真实类型 > - **方式三(Class.forName)**:字符串传类名,最灵活,**是框架最常用的方式**;注意**全类名写错或类不存在时抛 `ClassNotFoundException` 异常(受检异常,必须要处理)** ### Demo05 代码演示:三种方式 + 验证是同一个对象 ```java // 来源:course/Demo05.java public class Demo05 { public static void main(String[] args) throws ClassNotFoundException { //方式一:类名.class 编译期确定,最安全 Class clazz1 = Student.class; Class integerClass = int.class; Class integerClass1 = Integer.TYPE; System.out.println(integerClass1==integerClass); //true int[] arr=new int[10]; Class aClass = arr.getClass(); Class aClass1 = int[].class; //方式二:对象.getClass() 需要有对象 Object student=new Student(); Class clazz2 = student.getClass(); //方式三:Class.forName("全类名") Class clazz3 = Class.forName("course.Student"); //三种方式获取的到的对象是同一个对象,也就是 clazz1==clazz2==clazz3 System.out.println("clazz1==clazz2 : "+ (clazz1==clazz2)); System.out.println("clazz2==clazz3 : "+ (clazz2==clazz3)); System.out.println("clazz3==clazz1 : "+ (clazz3==clazz1)); System.out.println(clazz1); System.out.println(clazz2); System.out.println(clazz3); //通过Class对象直接查看完整的类型信息 System.out.println("类名:"+clazz1.getName()); System.out.println("简单类名:"+clazz1.getSimpleName()); System.out.println("父类:"+clazz1.getSuperclass().getName()); System.out.println("包名:"+clazz1.getPackage().getName()); // Person.class不会加载静态代码块 //Class personClass = Person.class; //当前案例看不出来,但是反序列化就可以看出对象.getClass()不加载静态代码块 // Class personClass = new Person().getClass(); //forName("course.Person") 一定会触发静态代码块 Class.forName("course.Person"); } } ``` ### 预期输出与运行要点 ``` true clazz1==clazz2 : true clazz2==clazz3 : true clazz3==clazz1 : true class course.Student class course.Student class course.Student 类名:course.Student 简单类名:Student 父类:java.lang.Object 包名:course 人类的静态代码块............. ← 最后一行 Class.forName("course.Person") 触发的静态代码块 ``` | 代码 | 结果/说明 | |------|-----------| | `int.class == Integer.TYPE` | 输出 **true**——基本类型(int/double/byte…)也有对应的 Class 对象 | | `arr.getClass()` / `int[].class` | **数组也有 Class 对象** | | `clazz1 == clazz2 == clazz3` | 三个判断都输出 **true**——三种方式拿到的是**同一个对象**(JVM 每个类只加载一份字节码) | | `clazz1.getName()` | 输出 `course.Student`——**全类名** | | `clazz1.getSimpleName()` | 输出 `Student`——**简单类名** | | `clazz1.getSuperclass().getName()` | 输出 `java.lang.Object`——**父类的类名** | | `clazz1.getPackage().getName()` | 输出 `course`——**包名** | | `Class.forName("course.Person")` | **一定会触发静态代码块**——输出"人类的静态代码块............." | ### 重要知识点:类加载的时机差异 ```java // 来源:course/Demo05.java(类注释) /* 注意: forName()传错全类名会抛异常ClassNotFoundException异常 基本类型(int\double\byte...)也有对应的Class对象 int.class int.class == Integer.TYPE 数组也有Class对象 int[].class 类加载的时机: Class.forName()在返回Class之前会执行类的静态初始化块(static块); 类名.class和对象.getClass不会触发静态代码块 */ ``` | 获取方式 | 是否触发静态代码块 | 原因 | |----------|--------------------|------| | **方式三:Class.forName("全类名")** | **会触发**静态代码块 | `Class.forName()` 在返回 Class 之前会**执行类的静态初始化块(static 块)**——所以 Demo05 最后一行 `Class.forName("course.Person")` 打印了"人类的静态代码块" | | **方式一:类名.class** | **不会触发** | `Person.class` 只是获取 Class 对象,**不执行静态代码块** | | **方式二:对象.getClass()** | **不会触发** | 当前案例看不出,但反序列化等场景可验证 `对象.getClass()` **不加载静态代码块** | > **要点**: > - **基本类型也有 Class 对象**:`int.class`,并且 `int.class == Integer.TYPE` 结果为 **true** > - **数组也有 Class 对象**:`int[].class`、`arr.getClass()` 都能拿到 > - **Class.forName 会触发类的静态初始化**(执行 static 块)——这是它与另外两种方式最重要的区别,框架"通过字符串类名加载并初始化类"正是基于这一点 > - **forName 传错全类名**:抛 `ClassNotFoundException` 异常(受检异常,必须处理) > - **通过 Class 对象可查看完整类型信息**:`getName()`(全类名)/ `getSimpleName()`(简单类名)/ `getSuperclass()`(父类)/ `getPackage()`(包名) ### 辅助演示类 Student / Person / Animal ```java // 来源:course/Student.java(本文件内定义三个类) public class Student{ private Integer id; private String name; private Integer age; private String gender; public void study(){ System.out.println(name+"在学习....."); } } class Person extends Animal{ static{ System.out.println("人类的静态代码块............."); } } class Animal{ } ``` | 类 | 用途 | |----|------| | **Student** | 反射的**演示目标类**:私有字段 id/name/age/gender + 方法 study()——字段用 `private` 修饰,正是反射能"获取私有成员"的体现 | | **Person**(extends Animal) | 演示 **Class.forName 触发静态代码块**:Person 的 static 代码块会打印"人类的静态代码块",Demo05 最后一行 `Class.forName("course.Person")` 触发它 | | **Animal** | Person 的父类,空的占位类——用于演示继承关系(`getSuperclass()` 可拿到父类) | > **要点**: > - 一个 `.java` 文件里可以定义多个类:`Student` 是 `public` 类(必须与文件名一致),`Person`、`Animal` 是**包级私有类**(默认访问修饰符) > - `Person extends Animal` 体现了继承关系——为后续用反射获取"父类信息"(`getSuperclass()`)提供素材 > - `Person` 的静态代码块"人类的静态代码块"是验证 `Class.forName()` 触发类加载"初始化"阶段的**直观证据** --- ## 11. 反射 Constructor 构造方法:获取与暴力反射(Demo06 + Student) ### 概念 —— 拿到 Class 对象后,反射最核心的用途:获取构造方法创建对象 当我们拿到了 Class 对象后,就可以通过这个对象**获取构造方法,并且用构造方法创建对象**——这也是**反射在框架中最核心的用途**。例如 **Spring 框架的 IOC**,就是根据配置文件提供的类名,通过反射获取构造方法创建对象。 ```java // 来源:course/Demo06.java(类注释) /* 当我们拿到了Class对象后,就可以通过这个对象获取构造方法,并且用构造方法创建对象,这也是反射在框架中最核心的用途。 Spring 框架 IOC 就是根据配置文件提供的类名,通过反射获取构造方法创建对象。 public Constructor[] getConstructors() 获取所有public构造方法对象数组 public Constructor getConstructor(Class... parameterTypes) 返回指定的单个public构造方法对象 public Constructor[] getDeclaredConstructors() 获取所有构造方法对象数组(包括private) public Constructor getDeclaredConstructor(Class... parameterTypes) 返回指定的单个构造方法对象(包括private) 包含 Declared 的就是获取所有的,包括私有化的。不含 Declared 的就是获取public修饰的 Constructor对象的方法: setAccessible(true) 允许访问私有构造方法 */ ``` ### 获取构造方法的 4 个 API —— Declared 决定能否拿到私有成员 | 方法 | 作用 | 能否获取 private 构造方法 | |------|------|---------------------------| | `getConstructors()` | 获取**所有 public 构造方法**对象数组 | ❌ 只能拿到 public | | `getConstructor(Class... parameterTypes)` | 返回**指定的单个 public 构造方法**对象 | ❌ 只能拿到 public | | `getDeclaredConstructors()` | 获取**所有构造方法**对象数组(**包括 private**) | ✅ 含 private | | `getDeclaredConstructor(Class... parameterTypes)` | 返回**指定的单个构造方法**对象(**包括 private**) | ✅ 含 private | > **要点**: > - **记忆口诀**:**包含 `Declared` 的就是获取所有的(包括私有化的);不含 `Declared` 的就是获取 public 修饰的** > - **参数写法**:`parameterTypes` 传构造方法的**参数类型 Class 数组/可变参数**——如 `getDeclaredConstructor(String.class, Integer.class)` 表示获取参数为 `(String, Integer)` 的构造方法 > - **Constructor 对象的方法**:`setAccessible(true)` **允许访问私有构造方法**(暴力反射的关键一步) ### 暴力反射 —— setAccessible(true) 绕过 private 限制 通过 `getDeclaredConstructors()` 可以拿到**私有的构造方法**,但直接 `newInstance()` 会因 private 权限报错;需要先调用 **`setAccessible(true)`** 放开访问权限,才能用私有构造方法创建对象——这种强行利用反射获取类的私有成员并操作私有成员的情况,称为**暴力反射**。 ``` 私有的构造方法对象(getDeclaredConstructor 获取) ↓ 直接 newInstance() ❌ 抛 IllegalAccessException(私有成员不可访问) ↓ setAccessible(true) 放开访问权限 ✅ ↓ newInstance(参数) 创建对象 ``` ### Demo06 代码演示:获取构造方法 + 暴力反射 ```java // 来源:course/Demo06.java package course; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; /** * @author WanJl * @version 1.0 * @title Demo06 * @description 反射-Constructor构造方法 * @create 2026/8/12 */ public class Demo06 { /* ……(上述构造方法反射理论写在类注释中)…… */ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // 根据 Student 类的全类名,获取Student的Class对象 Class studentClass = Class.forName("course.Student"); // 根据studentClass对象获取Student的构造方法--获取所有public修饰的构造方法 Constructor[] constructors = studentClass.getConstructors(); System.out.println(Arrays.toString(constructors)); //获取Student的public修饰的无参构造方法 Constructor constructor = studentClass.getConstructor(); //通过获取到的无参构造方法的对象,创建Student的对象 Object o = constructor.newInstance(); //判断这个o对象是不是Student类型 System.out.println("这个对象是Student类型吗?"+(o instanceof Student)); //强转 Student s=(Student) o; //调用方法 s.study(); //---这种强行利用反射获取类的私有成员并操作私有成员的情况--暴力反射 //根据studentClass对象获取Student的构造方法--包括private的 Constructor[] declaredConstructors = studentClass.getDeclaredConstructors(); System.out.println("构造方法个数:"+declaredConstructors.length); //获取Student的private修饰的有参数参构造方法 Constructor declaredConstructor = studentClass.getDeclaredConstructor( String.class,Integer.class); //允许访问私有化成员 declaredConstructor.setAccessible(true); //通过对象创建Student的对象,调用的是Student私有的有参构造方法 Object o1 = declaredConstructor.newInstance("张三", 1001); if (o1 instanceof Student){ Student s1=(Student) o1; s1.study(); } } } ``` ### Student 类新增构造方法(Demo06 的反射目标) ```java // 来源:course/Student.java(本文件内新增 3 个构造方法) public class Student{ private Integer id; private String name; private Integer age; private String gender; public Student() { // ① public 无参构造 } private Student(String name, Integer id) { // ② private 有参构造(暴力反射目标) this.name = name; this.id = id; } public Student(Integer id, String name, Integer age, String gender) { // ③ public 四参构造 this.id = id; this.name = name; this.age = age; this.gender = gender; } public void study(){ System.out.println(name+"在学习....."); } } ``` ### 预期输出与运行要点 ``` [java.lang.reflect.Constructor[..]@.., .., ..] ← getConstructors() 打印 public 构造方法数组(有 ①②...实际 2 个 public:无参 + 四参) 这个对象是Student类型吗?true ← getConstructor().newInstance() 创建的对象是 Student 类型 null在学习..... ← 无参构造 name 为 null,study() 输出 null在学习..... 构造方法个数:3 ← getDeclaredConstructors() 拿到全部 3 个构造方法(含 private 的 2 参构造) 张三在学习..... ← setAccessible(true) + newInstance("张三", 1001) 调用 private 构造创建对象 ``` | 代码 | 作用 | 说明 | |------|------|------| | `Class.forName("course.Student")` | 获取 Student 的 Class 对象 | 方式三(全类名,框架最常用),回顾第 10 节 | | `studentClass.getConstructors()` | 获取**所有 public 构造方法**数组 | 不含 private,输出数组内容(用 `Arrays.toString` 打印) | | `studentClass.getConstructor()` | 获取 **public 无参构造** | 参数为空表示无参构造 | | `constructor.newInstance()` | **用构造方法对象创建对象** | 反射创建对象的方式,等价于 `new Student()` | | `o instanceof Student` | 判断创建的对象类型 | 输出 true——反射创建的对象确实是 Student 类型 | | `(Student) o` 强转 + `s.study()` | 把 Object 强转成 Student 并调用方法 | 反射创建对象后转回具体类型调用业务方法 | | `studentClass.getDeclaredConstructors()` | 获取**所有构造方法**(含 private) | 拿到全部 3 个(public 无参 + private 2 参 + public 4 参) | | `getDeclaredConstructor(String.class, Integer.class)` | 获取指定的 **private 有参构造** | 参数类型匹配 `private Student(String, Integer)` | | `declaredConstructor.setAccessible(true)` | **放开私有成员的访问权限** | 暴力反射的关键一步,否则 newInstance 报权限错误 | | `newInstance("张三", 1001)` | 用 **private 构造**创建对象 | 成功创建 name=张三、id=1001 的 Student 对象 | > **要点**: > - **反射创建对象的两种路径**:`getConstructor().newInstance()`(public 构造)+ `getDeclaredConstructor().setAccessible(true).newInstance()`(private 构造) > - **暴力反射** = 强行利用反射获取类的私有成员并操作私有成员——`setAccessible(true)` 允许访问私有构造方法(这正是第 9 节"反射可能破坏封装性"的具体体现) > - **Declared 的口诀**:包含 Declared 的能拿到私有,不含的只能拿 public > - **方法签名抛 4 个受检异常**:`ClassNotFoundException`(forName 类名写错)/ `NoSuchMethodException`(找不到该参数类型的构造方法)/ `InstantiationException`(抽象类等无法实例化)/ `IllegalAccessException`(无访问权限)/ `InvocationTargetException`(被调方法内部抛异常)——调用反射 API 必须处理这些受检异常(main 用 throws 声明) > - **反射创建对象 vs new**:反射创建对象就是"运行时动态 new",是框架(Spring IOC 等)根据配置字符串类名创建对象的底层原理 --- ## 12. 知识点全景总结 | 知识点 | 核心内容 | 来源 | |--------|----------|------| | **Java 程序执行全流程** | `.java` 源文件 → 编译 → `.class` 字节码 → 类加载器 → 内存 → JVM 解释 → 二进制机器码 → 执行 | course/Demo01 注释 | | **类加载器的作用** | 把 `.class` 文件加载到内存中——字节码的"搬运工" | course/Demo01 注释 | | **类加载 6 种时机** | ①创建实例对象 ②调用 static 方法 ③访问/赋值 static 变量 ④反射创建 Class 对象 ⑤初始化子类 ⑥Java.exe 运行主类 | course/Demo01 注释 | | **懒加载原则** | 使用这个类的时候才会加载,不使用就不加载 | course/Demo01 注释 | | **全限定名** | 包名.类名(如 `java.lang.Object`),类加载用它定位字节码 | course/Demo01 注释 | | **加载(Loading)** | 通过全限定名获取二进制字节流 → 转为运行时数据结构 → 生成 `java.lang.Class` 对象 | course/Demo01 注释 | | **链接-验证(Verify)** | 检查字节流符合虚拟机规范、不会危害虚拟机安全 | course/Demo01 注释 | | **链接-准备(Prepare)** | 为 static 类变量分配内存并设置**默认初始化值** | course/Demo01 注释 | | **链接-解析(Resolve)** | 符号引用替换为直接引用;用到其他类型在此查找,找不到报错 | course/Demo01 注释 | | **初始化(Init)** | 按主观计划给静态变量赋值、执行静态代码块等 | course/Demo01 注释 | | **Demo01 执行顺序** | **静态代码块(a=10000)→ 代码块(a=100)→ 构造方法(a=10)**——类加载先于实例化 | course/Demo01 main | | **三种类加载器** | BootstrapClassLoader(虚拟机内置,表示为 null,无父)/ PlatformClassLoader(JDK 特殊模块)/ SystemClassLoader(用户类路径类库) | course/Demo02 注释 | | **双亲委派层级** | SystemClassLoader 继承 PlatformClassLoader 继承 BootstrapClassLoader | course/Demo02 注释 | | **Demo02 获取演示** | `ClassLoader.getSystemClassLoader()` → `getParent()` 逐级获取;Bootstrap 打印 null,对其调 `getParent()` 抛空指针 | course/Demo02 main | | **jar 包** | 加载的不一定是 .class 文件,也可能是 jar 包——把 Java/class 文件封装成可执行的程序包,让别人使用 | course/Demo01 注释 | | **双亲委派模型** | 收到加载请求不自己先加载,先委派给父类加载器、递归至最顶层;顶层能加载就成功返回,顶层无法完成才由子类加载器尝试自己加载 | course/Demo03 注释 | | **委派方向与回退方向** | 委派向上(子→父→祖父→最顶层);回退向下(顶层→……→子),我们的类最终由 SystemClassLoader 加载 | course/Demo03 注释 | | **双亲委派原因①:避免重复加载** | 同一个类全 JVM 只有一个 Class 对象(唯一性由类+类加载器共同决定),双亲委派保证任意一个类只被加载一次 | course/Demo03 注释 | | **双亲委派原因②:核心类库安全** | 防止用户写假的"核心类"混入;java.lang.String 只能由顶层启动类加载器加载,用户自定义同名类永远不会被加载 | course/Demo03 注释 | | **双亲委派原因③:保证类的唯一性** | 即使自定义加载器加载自定义 String,实际加载的还是顶层加载的核心类,避免多份不同 String 类型混乱 | course/Demo03 注释 | | **getResourceAsStream** | `classLoader.getResourceAsStream("prop.properties")` 用类加载器加载资源文件,参数为 classpath 下文件路径,返回字节流 | course/Demo03 main | | **Properties** | 解析 properties 文件的对象:父类是 Hashtable、是 Map 接口的另一个实现类、底层是数组;`load(InputStream)` 加载、`getProperty(key)` 按 key 取值 | course/Demo03 main | | **prop.properties 配置** | 数据库连接四要素:db.driver(MySQL 驱动)/ db.url(JDBC 连接地址)/ db.username(用户名,占位符示例)/ db.password(密码,占位符示例) | src/prop.properties | | **配置文件的意义** | 配置与代码分离:改配置不用改代码;加载后调用连接数据库方法;关闭流;常见格式还有 xml / json / yaml | course/Demo03 main | | **反射(Reflection)概念** | Java 的运行时动态机制:运行状态中获取任意类的所有属性方法(含 private)、动态调用任意对象的属性和方法;普通方式代码写死、反射方式动态;是 Spring/MyBatis/Hibernate/JDBC 等框架的基础 | course/Demo04 注释 | | **普通调用 vs 反射调用 5 维对比** | 获取信息时机(编译期写死 vs 运行期动态)/ 依赖关系(强耦合 vs 解耦)/ 创建对象(new 类名() vs Class.forName()+newInstance())/ 调用方法(对象.方法名() vs method.invoke(对象,参数))/ 灵活性(固定 vs 可动态切换) | course/Demo04 注释 | | **为什么需要反射 3 场景** | ①配置驱动:字符串类名→运行时反射翻译实例化 ②通用工具:打印任意对象全部属性 ③第三方类库解耦:只定义接口、运行时动态加载具体实现 | course/Demo04 注释 | | **反射的注意事项** | 学习反射为理解框架底层源码、架构设计/中间件解耦适配;日常业务尽量不用:①可能破坏封装性 ②效率比正常创建对象低 3~100 倍 | course/Demo04 注释 | | **反射的工作机制** | .class 字节码 → 类加载器加载进 JVM → 每类生成唯一 java.lang.Class 对象("类的类模板"记录类名/父类/接口/字段/方法/构造方法)→ 反射一切操作从 Class 出发:Constructor.newInstance() 创建对象 / Field.get/set 读写字段 / Method.invoke() 执行方法 | course/Demo04 注释 | | **反射核心类** | Class(java.lang.Class 反射入口)/ Constructor(java.lang.reflect.Constructor 创建对象)/ Field(java.lang.reflect.Field 读写字段)/ Method(java.lang.reflect.Method 调用方法)/ Modifier(解析修饰符)/ Parameter(方法参数) | course/Demo04 注释 | | **获取 Class 对象方式一** | 类名.class:最直接最简单、编译期类型检查不允许运行时才失败、前提是编译期知道类名 | course/Demo05 注释 | | **获取 Class 对象方式二** | 对象.getClass():Object 类定义的方法、所有对象能调用、需先有对象实例、适合运行时判断对象真实类型 | course/Demo05 注释 | | **获取 Class 对象方式三** | Class.forName("全类名"):静态方法、字符串传类名最灵活(变量/配置文件/网络数据)、框架最常用;全类名写错抛 ClassNotFoundException 受检异常 | course/Demo05 注释 | | **三种方式殊途同归** | 同一个类三种方式得到同一个对象(clazz1==clazz2==clazz3 全为 true)——JVM 对每个类只加载一份字节码 | course/Demo05 main | | **基本类型与数组的 Class** | 基本类型也有 Class 对象:int.class == Integer.TYPE 为 true;数组也有 Class 对象:int[].class、arr.getClass() | course/Demo05 main | | **类加载时机差异** | Class.forName() 返回前会执行类的静态初始化块(static 块);类名.class 和对象.getClass() 不触发静态代码块 | course/Demo05 注释/main | | **Class 对象查看类型信息** | getName() 全类名 / getSimpleName() 简单类名 / getSuperclass() 父类 / getPackage() 包名 | course/Demo05 main | | **Student / Person / Animal 演示类** | Student(id/name/age/gender 私有字段 + study() 方法,反射演示目标);Person extends Animal(静态代码块"人类的静态代码块",验证 forName 触发静态初始化);Animal(空父类) | course/Student.java | | **BootstrapClassLoader 为何是 null** | 由 C/C++ 实现、是 JVM 的一部分(不属于 Java 类),所以 Java 调用输出返回的就是 null(Demo02 注释中已标注) | course/Demo02.java | | **反射最核心用途:获取构造方法创建对象** | 拿到 Class 对象后可通过构造方法创建对象;Spring 框架 IOC 就是根据配置文件类名通过反射获取构造方法创建对象 | course/Demo06 注释 | | **获取构造方法 4 个 API** | getConstructors()(所有 public 数组)/ getConstructor(Class...)(指定 public)/ getDeclaredConstructors()(所有含 private 数组)/ getDeclaredConstructor(Class...)(指定含 private);口诀:**含 Declared 的能拿私有,不含的只能拿 public** | course/Demo06 注释 | | **暴力反射与 setAccessible(true)** | 强行利用反射获取类的私有成员并操作私有成员=暴力反射;`setAccessible(true)` 允许访问私有构造方法,否则 newInstance 报权限错误 | course/Demo06 main | | **反射创建对象 newInstance()** | `constructor.newInstance()` 反射创建对象(等价运行时 new);`getConstructor().newInstance()` 用 public 构造、`getDeclaredConstructor().setAccessible(true).newInstance()` 用 private 构造;`instanceof` 判断对象类型、强转后调用方法 | course/Demo06 main | | **Student 新增 3 个构造方法** | public 无参 / private (String name, Integer id)(暴力反射目标)/ public (Integer id, String name, Integer age, Integer gender) | course/Student.java | | **反射 API 的受检异常** | 反射方法签名抛多个受检异常:ClassNotFoundException / NoSuchMethodException / InstantiationException / IllegalAccessException / InvocationTargetException——必须处理(main 用 throws 声明) | course/Demo06 main | --- ## 13. 随堂练习要点 - **执行全流程复述(必做)**:把 `.java 文件 → 编译 → .class 字节码文件 → 类加载器 → 内存 → JVM 解释 → 二进制机器码 → 执行` 这条链背下来——这是理解本课所有知识点的总纲 - **类加载 6 种时机背诵**:①创建实例对象 ②调用 static 方法 ③访问/赋值 static 变量 ④反射创建 Class 对象 ⑤初始化子类 ⑥Java.exe 运行主类——每一类都能举一个代码例子(new / Math.abs / Student.name / Class.forName / new 子类 / 运行 main 类) - **懒加载理解**:思考"为什么 Java 要设计成使用时才加载"——省内存、加快启动速度;对比"启动时把所有类全部加载"的坏处(内存浪费、启动慢) - **加载过程三步背诵(必做)**:加载 → 链接(验证 / 准备 / 解析)→ 初始化;能说出每步一句话(加载=搬文件生成 Class 对象 / 验证=安全检查 / 准备=static 变量分内存设默认值 / 解析=符号引用换直接引用 / 初始化=执行我们写的静态初始化代码) - **准备 vs 初始化区分**:准备阶段 static int a 只是**默认值 0**,初始化阶段才执行 `a = 10` 这类赋值——思考 Demo01 中 static 变量 a 的"默认 0 → 10000"两步 - **Demo01 执行顺序实验(必做)**:运行 `course/Demo01`,观察输出顺序为**静态代码块(10000) → 代码块(100) → 构造方法(10)**;再在 main 中**连续 new 两个 Demo01 对象**,观察静态代码块是否只打印一次(类加载只发生一次) - **删静态代码块实验**:把 Demo01 的 static 代码块注释掉,重新运行,观察输出变化——体会"静态代码块属于类加载阶段、只执行一次" - **三种类加载器背诵**:BootstrapClassLoader(虚拟机内置 / 表示为 null / 无父)/ PlatformClassLoader(JDK 特殊模块)/ SystemClassLoader(用户类路径类库)——层级:系统 → 平台 → 启动 - **Demo02 运行验证(必做)**:运行 `course/Demo02`,观察三个打印结果——系统类加载器 `AppClassLoader`、平台类加载器 `PlatformClassLoader`、启动类加载器 **null**;最后一行 `getParent()` 触发**空指针异常** - **空指针原因思考**:为什么"虚拟机类加载器还有没有父类加载器?"会空指针——因为 bootstrap 表示为 null,对 null 调方法必然 NPE;这也印证了"启动类加载器没有父加载器" - **双亲委派理解**:把"SystemClassLoader → 父 PlatformClassLoader → 父 BootstrapClassLoader"的委派链画出来——思考我们的 `course.Demo01` 是由哪个加载器加载的(系统类加载器 SystemClassLoader) - **javap 观察字节码(拓展)**:对 `Demo01.class` 执行 `javap -verbose Demo01`,观察字节码中的"符号引用 / 常量池"——为理解第 4 节"解析:符号引用替换为直接引用"提供直观素材 - **双亲委派流程复述(必做)**:用"加载 course.Demo01"走一遍双亲委派——系统类加载器收到请求 → 先委派给平台类加载器 → 再委派给启动类加载器 → 顶层加载不了 → 逐级向下回退 → 最终系统类加载器加载成功;记住两个方向:**委派向上、回退向下** - **委派动作实验**:把第 6 节 Demo02 的代码扩展——在 `main` 里打印 `System.out.println(Demo01.class.getClassLoader())`,观察**我们自己写的类由系统类加载器加载**;再打印 `System.out.println(String.class.getClassLoader())`,观察核心类加载器为 **null**(启动类加载器加载,Java 中表示为 null)——一对比就理解"核心类归顶层、用户类归底层" - **双亲委派三个原因背诵(必做)**:①避免类被重复加载(全 JVM 一个类只有一个 Class 对象)②保证核心类库安全(用户写的 java.lang.String 永不加载)③保证类的唯一性(避免多份 String 类型混乱)——每个原因都能一句话说清 - **假 String 实验(思考)**:尝试自己写一个 `java.lang.String` 类放在 classpath 里并运行,观察程序**报错(找不到 main 方法 / 加载的还是官方 String)**——直观体会"用户自定义同名核心类永远不会被加载" - **类的唯一性理解**:记住公式"**类的唯一性 = 类 + 类加载器共同决定**"——同一个类名由不同类加载器加载,产生的是不同的 Class 对象;双亲委派从机制上避免了同一个类被重复加载 - **加载配置文件实验(必做)**:运行 `course/Demo03`,观察 `System.out.println(pro)` 输出 `{db.password=********, db.username=your_username, db.driver=com.mysql.cj.jdbc.Driver, db.url=jdbc:mysql://...}`——4 个数据库配置被成功读取 - **getResourceAsStream 路径实验**:把 `getResourceAsStream("prop.properties")` 的参数改成不存在的路径(如 `"abc.properties"`),运行观察返回的 inputStream 为 **null**、后续 `pro.load(null)` 抛**空指针异常**——体会"参数必须是 classpath 下真实存在的文件" - **Properties 回顾 Map 知识**:Properties 的父类是 Hashtable、是 Map 接口的实现类、底层是数组——回顾 07-31 学的 Map(HashMap/Hashtable)知识,理解 Properties 就是"key=value 的配置型 Map" - **getProperty 实验**:给 prop.properties **新增一行自定义配置**(如 `app.name=J260713`),在 Demo03 里用 `pro.getProperty("app.name")` 读取并打印——体会"改配置文件不用改代码"的配置与代码分离思想 - **关闭流练习**:把 `inputStream.close()` 注释掉再运行,体会资源不关闭的隐患(结合 08-06/08-07 IO 流"用完必须 close"的铁律)——后面学 try-with-resources 可自动关闭 - **数据库配置串联思考**:prop.properties 里的 db.driver(`com.mysql.cj.jdbc.Driver`)和 db.url(`jdbc:mysql://...`)正是 **JDBC 连接数据库**的标配参数——思考后续学 JDBC 时,这套"类加载器读配置文件"的方式如何复用 - **反射概念复述(必做)**:用一句话说清"什么是反射"——Java 的运行时动态机制,运行状态中能获取任意类的所有属性方法(包括 private)、调用任意对象的任意属性和方法;能说出"普通方式代码写死、反射方式动态,反射是 Spring/MyBatis 等框架的基础" - **普通 vs 反射 5 维对比背诵(必做)**:①获取信息时机(编译期写死 vs 运行期动态)②依赖关系(强耦合 vs 解耦)③创建对象(new vs Class.forName+newInstance)④调用方法(对象.方法 vs method.invoke)⑤灵活性(固定 vs 动态切换)——每个维度都能举一个代码例子 - **反射三个场景理解**:①配置驱动(配置文件字符串类名→反射实例化)②通用工具(打印任意对象全部属性——对应 Demo04 的 printUtil 方法)③第三方类库解耦(只定义接口、运行时动态加载实现)——思考生活中哪些框架用到了反射 - **反射的注意事项(必背)**:日常业务开发"一定一定一定"尽量不用反射——①可能破坏封装性(能拿 private)②效率比正常创建对象低 3~100 倍;学习反射的目的是理解框架底层源码 + 架构设计/中间件做解耦适配 - **反射工作链条复述(必做)**:把"Person.java → javac 编译 → Person.class 字节码 → 类加载器加载 → Person 的 Class 对象(类的类模板)→ Constructor.newInstance() 创建对象 / Field.get/set 读写字段 / Method.invoke() 调用方法"这条链背下来——反射一切操作从 Class 对象出发 - **反射核心类背诵**:Class(java.lang.Class 反射入口)/ Constructor(创建对象)/ Field(读写字段)/ Method(调用方法)/ Modifier(解析修饰符)/ Parameter(方法参数)——每个核心类记住所属包 java.lang.reflect - **Demo04 printUtil 思考**:看 Demo04 的 `printUtil(Object obj)` 方法注释"输出这个类的所有属性名--就必须要使用反射来实现"——思考为什么普通方式做不到(编译期不知道传入对象的具体类型) - **三种获取 Class 方式背诵(必做)**:①类名.class(编译期类型检查最安全)②对象.getClass()(Object 方法需有对象实例)③Class.forName("全类名")(字符串传类名最灵活、框架最常用)——能分别写出 Student.class / student.getClass() / Class.forName("course.Student") - **Demo05 运行验证(必做)**:运行 `course/Demo05`,观察三个判断 `clazz1==clazz2 / clazz2==clazz3 / clazz3==clazz1` 全部输出 **true**——印证"同一个类三种方式拿到同一个 Class 对象";再观察最后输出"人类的静态代码块"——印证 Class.forName 触发静态代码块 - **int.class == Integer.TYPE 实验**:在 Demo05 里打印 `int.class == Integer.TYPE` 观察输出 true——体会"基本类型也有 Class 对象"(回顾 07-29 包装类 Integer.TYPE 知识) - **数组 Class 实验**:在 Demo05 里打印 `int[].class.getName()` 观察输出 `[I`(JVM 中 int 数组的类型表示)——体会"数组也有 Class 对象" - **forName 触发静态代码块实验(必做)**:把 Demo05 最后一行改成 `Person.class`(注释掉 forName),重新运行观察"人类的静态代码块"不再打印——直观体会"类名.class 不触发静态代码块、forName 会触发";再用 `new Person().getClass()` 替换,观察也不打印(对象.getClass() 不触发) - **forName 异常实验**:把 `Class.forName("course.Student")` 的类名改错(如 `"course.StudentX"`),运行观察抛 **ClassNotFoundException**——体会"受检异常必须要处理"(方法签名已声明 throws) - **Class 对象查看类型信息(必做)**:用 `clazz1.getName()`(全类名 course.Student)/ `getSimpleName()`(Student)/ `getSuperclass().getName()`(java.lang.Object)/ `getPackage().getName()`(course)打印并理解每个方法的含义 - **Person extends Animal 理解**:Student.java 文件里 Person 继承 Animal 且带静态代码块——理解"一个 .java 文件可定义多个类(public 类与文件名一致,其他为包级私有类)",为后续反射获取父类信息做铺垫 - **反射与类加载衔接思考**:回顾第 3 节"类加载的 6 种时机"中的第 ④ 条"**反射创建 Class 对象**"——`Class.forName()` 既是获取 Class 对象的方式,也会触发类加载;本课"类加载机制 + 反射"两个主题通过 Class 对象紧密相连 - **反射与框架串联思考**:思考 Spring 框架的 IOC 容器、MyBatis 的 Mapper 代理、JDBC 驱动加载(Class.forName("com.mysql.cj.jdbc.Driver"))——这些框架底层都是"读取配置 → Class.forName → 反射实例化/调用"的套路,后续学习框架时会反复见到 - **获取构造方法 4 个 API 背诵(必做)**:`getConstructors()`(所有 public 数组)/ `getConstructor(Class...)`(指定 public)/ `getDeclaredConstructors()`(所有含 private 数组)/ `getDeclaredConstructor(Class...)`(指定含 private)——背口诀:**含 Declared 的能拿私有(包含所有),不含 Declared 的只能拿 public** - **Demo06 运行验证(必做)**:运行 `course/Demo06`,观察输出——`getConstructors()` 打印 public 构造数组、`o instanceof Student` 输出 true、无参构造创建的 name 为 null(study 输出 null在学习.....)、`getDeclaredConstructors()` 输出"构造方法个数:3"、暴力反射后"张三在学习....."——完整走一遍"获取构造方法→创建对象→强转调用方法" - **暴力反射实验(必做)**:把 Demo06 中 `declaredConstructor.setAccessible(true)` 一行**注释掉**再运行,观察抛 **IllegalAccessException**——直观体会"不 setAccessible(true) 无法访问 private 构造方法"、理解暴力反射为何需要放开权限 - **getConstructor 参数实验**:尝试用 `studentClass.getConstructor(String.class, Integer.class)`(不带 Declared)获取 private 2 参构造,观察抛 **NoSuchMethodException**——体会"不含 Declared 只能拿 public"的口诀 - **newInstance 创建对象对比**:对比"反射创建对象 `getConstructor().newInstance()`" 与 "普通 new Student()"——思考反射创建对象就是"运行时动态 new"(Spring IOC 根据配置字符串类名动态创建对象的原理) - **instanceof + 强转练习**:反射创建的对象是 Object 类型,用 `instanceof Student` 判断后用 `(Student) o` 强转再调用 study()——回顾 07-24 多态向下转型知识 - **构造方法个数验证**:数一数 Student 类现有 3 个构造方法(public 无参 / private 2 参 / public 4 参),与 `getDeclaredConstructors().length` 输出"构造方法个数:3"对照——体会 Declared 拿到的是"全部构造方法" - **反射受检异常处理**:观察 Demo06 main 方法签名 `throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException`——理解反射 API 抛多个受检异常必须处理(回顾 07-28 异常处理:受检异常 vs 非受检异常) --- ## 14. 拓展阅读 - **类加载机制的地位**:本课(类的加载机制 + 类加载器)属于 **JVM(Java 虚拟机)** 知识领域——面试高频考点,也是理解反射、动态代理、Tomcat 类隔离、Spring 框架等进阶知识的基石 - **双亲委派模型(Parent Delegation Model)**:除了 JDK 三种内置加载器外,还可以**自定义类加载器**(继承 ClassLoader 并重写 `findClass()`);双亲委派模型保证**核心类库只由启动类加载器加载**,避免用户自定义同名类(如自己写个 `java.lang.String`)破坏 JDK 核心类 - **双亲委派的作用**:①**安全性**——核心类由启动加载器加载,防止核心类被篡改;②**避免重复加载**——父加载器已加载的类,子加载器不会重复加载;③**一致性**——保证同一个类在全 JVM 中只有一份 Class 对象 - **打破双亲委派**:Tomcat、JDBC(SPI 机制)、热部署等场景需要**打破双亲委派**(如 Tomcat 的 WebAppClassLoader 先加载自己目录的类),是 JVM 进阶的重要话题 - **类加载与反射的衔接**:第 ④ 条加载时机"反射创建 Class 对象"——回顾 Java 反射:`Class.forName()` / `类名.class` / `对象.getClass()` 三种获取 Class 对象的方式,都触发了类加载;反射正是建立在"加载后生成 Class 对象"这一机制之上 - **Class 对象**:每个类被使用后,JVM 都会在**堆**中生成一个 `java.lang.Class` 类型的对象(类的"元信息":包名、方法、字段、父类等);`Class.forName("全限定名")` 可拿到它 - **对象的创建过程**:`new` 一个对象经历了**类加载(首次)→ 分配内存 → 实例变量默认初始化 → 普通代码块 → 构造方法**的完整链路——本课 Demo01 的"静态代码块 → 代码块 → 构造方法"正是这条链路的前半段直观演示 - **JDK 类加载器源码**:JDK9+ 中类加载器在 `jdk.internal.loader.ClassLoaders` 类中定义——`AppClassLoader`(系统/应用)、`PlatformClassLoader`(平台)、`BootstrapClassLoader`(启动)内部类;通过 `ClassLoader.getSystemClassLoader()` 即可获取 - **启动类加载器为何是 null**:BootstrapClassLoader 由 C++ 实现、是 JVM 的一部分(不属于 Java 类),因此 Java 代码中"拿不到它",统一用 **null** 表示——`getParent()` 返回 null 即代表"已到最顶层,没有父加载器" - **链接 vs 加载的时机**:加载、验证、准备、解析、初始化这 5 步并非严格串行——**解析**在某些情况下可以推迟到真正使用符号引用时才进行("惰性解析"),初始化则严格在类被"主动使用"时触发 - **类加载与 static 的关系**:static 变量/静态代码块属于**类级**(类加载的初始化阶段执行);普通代码块/构造方法属于**实例级**(每次 new 执行)——这就是 Demo01 执行顺序(静态先于实例)的底层依据 - **类加载与线程安全**:JVM 保证**一个类的初始化过程在同一时间只会被一个线程执行**(初始化加了同步锁)——多线程同时首次使用一个类时,只有一个线程执行初始化,其余线程阻塞等待 - **双亲委派的三个价值小结**:①**安全**——核心类只能由启动加载器加载,用户自定义的假核心类(如 java.lang.String)永远不会被加载,防止核心 API 被篡改;②**唯一**——同一个类全 JVM 只有一个 Class 对象,避免类型混乱;③**不重复**——父加载器已加载的类子加载器不会重复加载 - **Class.getClassLoader() 验证**:`Class.getClassLoader()` 返回加载该类的类加载器——自己写的类返回系统类加载器(AppClassLoader);核心类(java.lang.String / java.lang.Object)返回 **null**(BootstrapClassLoader 用 null 表示)——这是验证"哪些类由哪个加载器加载"最直接的手段 - **JDBC 如何打破双亲委派**:JDBC(数据库驱动)是一个典型的**打破双亲委派**场景——`DriverManager` 是启动类加载器加载的**核心类**,但它需要加载用户 classpath 下的 **MySQL/Oracle 驱动 jar**(SystemClassLoader 加载的类);为此 JDK 提供了**线程上下文类加载器(Thread Context ClassLoader)**,让核心类可以反过来使用子加载器加载驱动类——理解 SPI 机制是 Java 进阶的经典话题 - **Tomcat 打破双亲委派**:Tomcat 的 `WebAppClassLoader` 先加载自己 Web 应用的类、加载不到才委派给父加载器(与双亲委派顺序相反)——从而实现**多个 Web 应用之间的类隔离**(不同应用可用不同版本的 Spring 等),也支持热部署 - **getResourceAsStream 的资源查找原理**:`ClassLoader.getResourceAsStream("prop.properties")` 会在 **classpath(类路径)** 下按给定路径查找资源文件——`src/` 目录编译后就在 classpath 中,所以放在 `src/` 根目录的文件能被找到;返回的 InputStream 就是文件的字节流 - **配置文件格式对比**:常用配置文件格式——**properties**(key=value 简单、Properties 解析)/ **xml**(标签树形结构、DOM/SAX 解析)/ **json**(对象嵌套、Jackson/Gson 解析)/ **yaml**(缩进层级、Spring Boot 默认配置格式)——不同格式各有适用场景,后续框架学习中会接触 xml/json/yaml - **配置与代码分离(环境隔离)**:把数据库连接、接口地址等**易变信息**放入配置文件,实现**配置与代码分离**——换数据库/换环境只改配置文件、不改代码不重新编译;企业开发还会按 **dev / test / prod 环境**准备多套配置文件,通过参数切换 - **JDBC 四要素**:连接 MySQL 数据库需要 4 个核心参数——**driver**(驱动类 `com.mysql.cj.jdbc.Driver`)、**url**(`jdbc:mysql://主机:端口/库名?参数`)、**username**(用户名)、**password**(密码)——正是 prop.properties 中配置的 4 项,后续学习 JDBC 时这套"配置文件读取参数"的方式会直接复用 - **try-with-resources 自动关闭**:本课 `inputStream.close()` 是手动关闭流;Java 7 提供了 **try-with-resources** 语法(`try (InputStream in = ...) { }`),实现 `AutoCloseable` 的资源会在 try 结束后**自动关闭**,无需手动 close——IO 流、网络连接等资源都推荐用它管理(后续课程会正式学习) - **反射在框架中的地位**:本课反射(Reflection)是 **Java 各种框架的底层基石**——Spring 的 IOC 容器(Bean 实例化)、MyBatis 的 Mapper 代理、Hibernate 的 ORM 映射、JDBC 驱动加载(`Class.forName("com.mysql.cj.jdbc.Driver")`)都建立在反射之上;理解反射 = 理解框架源码的第一把钥匙 - **Class 对象与类加载的关系**:反射建立在"类加载后生成 Class 对象"这一机制之上——本课两个主题紧密相连:**类加载机制**负责把 .class 字节码变成内存中的 Class 对象,**反射**负责从 Class 对象出发动态获取类的信息、动态调用方法;`Class.forName()` 既是"获取 Class 对象的三种方式"之一,也是第 3 节"类加载 6 种时机"中的第 ④ 条"反射创建 Class 对象" - **Class.forName vs ClassLoader.loadClass**:`Class.forName("全类名")` 默认会执行类的**静态初始化块**(初始化阶段);而 `ClassLoader.loadClass("全类名")` **默认不执行**静态初始化(只完成加载+链接,不初始化)——这是 JDBC 驱动加载用 forName 的原因(驱动需要在注册时执行静态块) - **getDeclared* 系列方法**:反射不仅能获取 public 成员,还能用 `getDeclaredFields()` / `getDeclaredMethods()` / `getDeclaredConstructors()` 获取**私有(private)成员**;配合 `setAccessible(true)` 可访问私有字段/调用私有方法——这正是"反射可能破坏封装性"的具体体现 - **反射的应用场景扩展**:反射在真实项目中的典型应用——①**动态代理**(JDK Proxy 底层基于反射的 InvocationHandler)②**Spring AOP**(切面拦截基于动态代理+反射)③**注解解析**(@Autowired/@RequestMapping 等注解通过反射读取)④**序列化框架**(Jackson/Gson 反射读写字段)⑤**开发工具**(IDEA 的代码补全、调试器的变量查看都基于反射) - **反射的性能优化**:反射效率低(本课提到 3~100 倍),实际框架中的优化手段——**缓存 Method/Field 对象**(避免重复 getMethod/getField)、**MethodHandle**(JDK7+ 更高效的动态调用方式)、**invokedynamic 指令**(JDK7+ 字节码层面优化,Lambda 表达式底层就是 invokedynamic)——理解这些有助于理解框架为何能"用反射但性能不差" - **反射与封装性/安全**:`setAccessible(true)` 可以绕过 Java 的访问控制(private 也能访问),这既是反射强大的原因,也是"破坏封装性"的根源——在某些安全要求高的场景(如模块化系统 JPMS)会限制反射的使用 - **反射三个核心成员**:本课 Demo06 已学 **Constructor(构造方法)**,反射三大核心成员还包含 **Field(字段属性)** 和 **Method(方法)**——后续课程会分别用 `getDeclaredFields()` / `getDeclaredMethods()` 获取私有字段与私有方法,套路与 Constructor 完全一致(Declared 拿私有 + setAccessible(true) 放开权限) - **Spring IOC 与反射**:Spring 框架的 IOC 容器**根据配置文件/注解中的类名字符串,通过反射获取构造方法创建 Bean 对象**(`Class.forName("com.xxx.UserService")` + `getDeclaredConstructor().newInstance()`)——这正是本课"反射是框架基础"论断的经典落地场景;理解反射构造方法 = 理解 Spring Bean 创建的第一步 - **newInstance 的两种写法**:JDK9 中 `Class.newInstance()` 被标记废弃(只能调用无参构造且权限检查严格),推荐使用 **`Constructor.newInstance()`**(可以传参、可以访问 private 构造)——本课 Demo06 正是后者 - **setAccessible(true) 的安全影响**:`setAccessible(true)` 本质是**绕过 Java 访问控制检查**(private 也能访问),在模块化系统(JPMS)中受 `--add-opens` 约束;反射框架(如 Spring)大量使用它来注入私有字段——这是"反射破坏封装性"与"框架必须用反射"的辩证关系 - **反射创建对象的效率与优化**:本课提到反射效率低 3~100 倍,但框架实际使用中会**缓存 Constructor/Method/Field 对象**(`getDeclaredConstructor` 只调用一次,后续复用 `newInstance`)来摊薄开销——这也是框架"用了反射但性能不差"的优化手段 - **JDK 文档**:[`java.lang.ClassLoader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ClassLoader.html)(类加载器核心 API,含 getResourceAsStream)、[`java.lang.Class`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html)(Class 对象:getName/getSimpleName/getSuperclass/getPackage 等)、[`java.lang.reflect.Constructor`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/reflect/Constructor.html)(构造方法反射:getConstructors/getDeclaredConstructors/newInstance/setAccessible)、[`java.lang.reflect.Field`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/reflect/Field.html)(字段反射)、[`java.lang.reflect.Method`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/reflect/Method.html)(方法反射)、[`java.util.Properties`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Properties.html)(配置文件解析类,父类 Hashtable)、[`Class.forName`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#forName(java.lang.String))(反射加载类)