c260812course我们写的 .java 源文件并不能直接运行,需要经过 编译 → 类加载 → JVM 解释 → 执行 四个阶段。Java 的"一次编写、到处运行"正依赖于这套机制:.java 编译成与平台无关的 .class 字节码,再由不同平台上的 JVM 解释执行。
// 来源: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 内存中
类加载器(ClassLoader) 的作用非常单一而重要:把 .class 文件加载到内存中。没有类加载器,字节码永远只是磁盘上的一堆文件,JVM 无法使用它们。
// 来源:course/Demo01.java(类注释)
/*
类加载器
类加载器的作用就是将就是把.class文件加载到内存中
加载 的时候,加载的不一定是.class文件,也可能是某个jar包,jar包就是把Java文件/class文件封装成可以执行的程序包,让别人使用
*/
类加载的输入并不局限于单个 .class 文件——加载的也可能是某个 jar 包。jar 包就是把多个 Java/class 文件封装成的可执行的程序包,方便打包分发、被别人引用使用(第三方框架如 MySQL 驱动、Spring 等都是 jar 包形式)。
要点:
- 类加载器处理的输入是
.class字节码文件(或 jar 包),输出是 内存中的 Class 对象- jar 包 = 把 Java/class 文件封装成可执行的程序包,让别人使用(项目的依赖、第三方库都是 jar 包)
- 这个过程发生在"编译之后、解释执行之前"
Java 的类加载遵循 "使用时才加载,不使用就不加载" 的懒加载原则。只有当你真正用到某个类时,JVM 才会把它加载进内存。类加载的时机(触发点)一共有 6 种:
// 来源: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 就会启动类加载流程
- 注意第 ⑤ 条:初始化子类会先触发父类的加载(先有父后有子,加载顺序从父到子)
类的加载过程大体分为 3 步(也可细分为 5 步):
③ 初始化(Initialization)
// 来源: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 变量"占位"分配内存并设默认值(此时还不是我们写的值)
- 解析 = 把"符号引用"(类的名字)换成"直接引用"(内存里的实际地址),用到别的类就在这步去找
- 初始化 = 真正执行静态变量赋值语句和静态代码块(我们写的初始化代码在这里跑)
- 小结一句:加载"把文件搬进来"、验证"检查安全"、准备"先分个默认内存"、解析"找到引用的类"、初始化"跑我们写的初始化代码"
Demo01 用静态变量 + 静态代码块 + 普通代码块 + 构造方法同时给 a 赋值并打印,可以直观看到类的加载(静态初始化)发生在实例化之前,以及类加载"初始化"阶段到底执行了什么:
// 来源: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 初始化"的区别
类加载时需要使用类加载器(ClassLoader)。JVM 内置了三种类加载器,分工明确、层级分明:
// 来源: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 内置的),它没有父加载器
- 代码里写的是"继承",本质是双亲委派:子加载器收到加载请求,先交给父加载器去加载
// 来源: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
双亲委派模型是三种类加载器之间协同工作的核心机制:一个类加载器收到类加载请求时,并不会自己先去加载,而是把请求委派给父类加载器执行;如果父类加载器还有父加载器,就继续向上委派,依次递归,直到最顶层的类加载器。若顶层类加载器能完成加载任务,就成功返回;若顶层无法完成(找不到该类),子类加载器才会尝试自己去加载。
// 来源: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(系统类加载器)加载
// 来源: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
类加载器除了加载字节码外,还有一个经典的实际应用——用类加载器加载资源配置文件。Demo03 演示了如何通过系统类加载器加载根目录下的 prop.properties 配置文件,再用 Properties 解析其中的数据库连接配置。
// 来源: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) {
}
}
# 来源: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 时直接用
反射(Reflection) 是 Java 语言提供的一种 运行时动态机制,它允许程序在 运行的状态 中:
这种 动态获取类的信息 + 动态调用对象方法 的功能,就是 Java 的反射机制。
// 来源:course/Demo04.java(类注释)
/*
反射
是Java语言提供的一种【运行时动态机制】,他允许程序在【运行的状态】中:
对 任意一个类 都能获取这个类的所有属性和方法(包括私有的成员private)
对 任意一个对象 都能够调用它的任意属性和方法。
这种 动态获取类的信息 + 动态调用对象方法 的功能 就是Java的反射机制。
*/
一句话总结:普通方式是代码写死的,反射方式是动态的。反射是 Java 中各种框架的基础——Spring、MyBatis、Hibernate、JDBC 驱动加载等等都离不开反射。
// 来源:course/Demo04.java(类注释)
/*
普通的调用 反射调用 的区别:
获取类信息的时机:
普通调用(编译期)- 就是在编译器写死,代码写死。
反射调用(运行期)- 运行时才会动态获取
依赖关系:
普通调用(编译期)- 编译期强耦合,耦合度高
反射调用(运行期)- 运行时才管理,解耦
创建对象:
普通调用(编译期)- new 类名()
反射调用(运行期)- Class.forname() + newInstance()
调用方法:
普通调用(编译期)- 对象.方法名()
反射调用(运行期)- method对象.invoke(对象,参数)
灵活性方面:
普通调用(编译期)- 固定不变
反射调用(运行期)- 高度灵活,可以动态切换
*/
| 对比维度 | 普通调用(编译期) | 反射调用(运行期) |
|---|---|---|
| 获取类信息的时机 | 在编译期写死,代码写死 | 运行时才会动态获取 |
| 依赖关系 | 编译期强耦合,耦合度高 | 运行时才管理,解耦 |
| 创建对象 | new 类名() |
Class.forName() + newInstance() |
| 调用方法 | 对象.方法名() |
method 对象.invoke(对象, 参数) |
| 灵活性 | 固定不变 | 高度灵活,可以动态切换 |
// 来源:course/Demo04.java(类注释)
/*
为什么需要反射?
如果没有反射,我们的程序需要使用某个类,就必须在编译期就要知道它的名字,然后使用new创建对象,使用. 调用方法。
这些在很多真实的场景中无法满足需求:
1、配置驱动:比如某个框架需要读取配置文件,文件里面写的时候字符串类型的类名,框架在运行的时候才会拿到这个字符串,
并且需要通过反射才能够把它翻译成真实的类并且进行实例化。
2、通用工具:写一个能够打印任意对象的全部属性的通用工具,调用方法后传入什么参数不确定,那么就只能通过反射来实现。
3、第三方类库的解耦:比如我们使用某个类库,Java只需要定义这个类库的规则(接口),类库自己实现具体的操作,
然后通过反射,在运行时动态加载,源码里面根本就不需要依赖某个具体的类。
*/
| 场景 | 说明 | 示例 |
|---|---|---|
| ① 配置驱动 | 配置文件里写的是字符串类型的类名,框架在运行时才拿到这个字符串,需要通过反射才能把它翻译成真实的类并实例化 | 框架读取配置文件 → Class.forName("com.xxx.Driver") |
| ② 通用工具 | 写一个能打印任意对象全部属性的通用工具,调用方法后传入什么参数不确定,只能通过反射实现 | printUtil(Object obj) 打印任意对象的所有属性名 |
| ③ 第三方类库解耦 | 使用某个类库时,Java 只需要定义这个类库的规则(接口),类库自己实现具体操作,然后通过反射在运行时动态加载——源码里根本不需要依赖某个具体的类 | 定义接口 + 运行时反射加载实现类 |
// 来源:course/Demo04.java(类注释)
/*
注意:我们需要学习反射,目的是1、学习框架的时候帮助我们理解框架的底层源码。2、当我们进行架构设计、开发中间件程序的时候需要使用反射做解耦和通用适配。
至于日常的业务开发中,一定一定一定尽量不要使用反射。开发使用反射,就相当于玩游戏使用金手指(开挂)
1、使用反射可能破坏封装性
2、使用反射的效率要远比正常创建对象效率低的多,最低3~100倍
*/
| 注意点 | 说明 |
|---|---|
| 学习反射的目的 | ① 学习框架时帮助理解框架的底层源码;② 架构设计、开发中间件程序时用反射做解耦和通用适配 |
| 日常业务开发 | 一定一定一定尽量不要使用反射——开发用反射就像玩游戏开挂(金手指) |
| 缺点 ① 破坏封装性 | 反射可以获取并调用私有成员(private),破坏类的封装性 |
| 缺点 ② 性能低 | 反射的效率远比正常创建对象低得多,最低 3~100 倍 |
// 来源: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 |
代表方法的参数 |
// 来源: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
获取 Class 对象的方式一共有 三种,它们殊途同归:同一个类无论通过哪种方式,得到的都是同一个对象——因为 JVM 对每个类只加载一份字节码。
// 来源:course/Demo05.java(类注释)
/*
获取Class对象的方式有三种,但是殊途同归,同一个类无论通过哪种方式,得到的都是同一个对象,JVM对每个类只加载一份字节码。
方式一: 类名.class
直接调用类名.class变量,这种方式最直接最简单,编译期就会进行类型检查,不允许运行时才失败。前提就是需要在编译期就知道类名。
Class<Object> objectClass = Object.class; //JVM需要在编译期就知道这个类是存在的,才行。
Class<String> stringClass = String.class;
方式二: 对象.getClass()
getClass()是Object类中定义的方法,所有的对象都能调用,这种方式需要先有一个对象实例,适合在已经拿到对象想要知道它是什么类型的时候使用。
Animal animal=new Teacher(); //父类的引用指向子类的对象--多态
Class<? extends Animal> 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异常(受检异常,必须要处理)
// 来源:course/Demo05.java
public class Demo05 {
public static void main(String[] args) throws ClassNotFoundException {
//方式一:类名.class 编译期确定,最安全
Class<Student> clazz1 = Student.class;
Class<Integer> integerClass = int.class;
Class<Integer> integerClass1 = Integer.TYPE;
System.out.println(integerClass1==integerClass); //true
int[] arr=new int[10];
Class<? extends int[]> aClass = arr.getClass();
Class<int[]> 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<Person> personClass = Person.class;
//当前案例看不出来,但是反序列化就可以看出对象.getClass()不加载静态代码块
// Class<? extends Person> 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") |
一定会触发静态代码块——输出"人类的静态代码块............." |
// 来源: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()(包名)
// 来源: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()触发类加载"初始化"阶段的直观证据
当我们拿到了 Class 对象后,就可以通过这个对象获取构造方法,并且用构造方法创建对象——这也是反射在框架中最核心的用途。例如 Spring 框架的 IOC,就是根据配置文件提供的类名,通过反射获取构造方法创建对象。
// 来源:course/Demo06.java(类注释)
/*
当我们拿到了Class对象后,就可以通过这个对象获取构造方法,并且用构造方法创建对象,这也是反射在框架中最核心的用途。
Spring 框架 IOC 就是根据配置文件提供的类名,通过反射获取构造方法创建对象。
public Constructor<?>[] getConstructors() 获取所有public构造方法对象数组
public Constructor<T> getConstructor(Class<?>... parameterTypes) 返回指定的单个public构造方法对象
public Constructor<?>[] getDeclaredConstructors() 获取所有构造方法对象数组(包括private)
public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回指定的单个构造方法对象(包括private)
包含 Declared 的就是获取所有的,包括私有化的。不含 Declared 的就是获取public修饰的
Constructor对象的方法:
setAccessible(true) 允许访问私有构造方法
*/
| 方法 | 作用 | 能否获取 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)允许访问私有构造方法(暴力反射的关键一步)
通过 getDeclaredConstructors() 可以拿到私有的构造方法,但直接 newInstance() 会因 private 权限报错;需要先调用 setAccessible(true) 放开访问权限,才能用私有构造方法创建对象——这种强行利用反射获取类的私有成员并操作私有成员的情况,称为暴力反射。
私有的构造方法对象(getDeclaredConstructor 获取)
↓ 直接 newInstance() ❌ 抛 IllegalAccessException(私有成员不可访问)
↓ setAccessible(true) 放开访问权限 ✅
↓ newInstance(参数) 创建对象
// 来源: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();
}
}
}
// 来源: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 等)根据配置字符串类名创建对象的底层原理
| 知识点 | 核心内容 | 来源 |
|---|---|---|
| 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 |
.java 文件 → 编译 → .class 字节码文件 → 类加载器 → 内存 → JVM 解释 → 二进制机器码 → 执行 这条链背下来——这是理解本课所有知识点的总纲a = 10 这类赋值——思考 Demo01 中 static 变量 a 的"默认 0 → 10000"两步course/Demo01,观察输出顺序为静态代码块(10000) → 代码块(100) → 构造方法(10);再在 main 中连续 new 两个 Demo01 对象,观察静态代码块是否只打印一次(类加载只发生一次)course/Demo02,观察三个打印结果——系统类加载器 AppClassLoader、平台类加载器 PlatformClassLoader、启动类加载器 null;最后一行 getParent() 触发空指针异常course.Demo01 是由哪个加载器加载的(系统类加载器 SystemClassLoader)Demo01.class 执行 javap -verbose Demo01,观察字节码中的"符号引用 / 常量池"——为理解第 4 节"解析:符号引用替换为直接引用"提供直观素材main 里打印 System.out.println(Demo01.class.getClassLoader()),观察我们自己写的类由系统类加载器加载;再打印 System.out.println(String.class.getClassLoader()),观察核心类加载器为 null(启动类加载器加载,Java 中表示为 null)——一对比就理解"核心类归顶层、用户类归底层"java.lang.String 类放在 classpath 里并运行,观察程序报错(找不到 main 方法 / 加载的还是官方 String)——直观体会"用户自定义同名核心类永远不会被加载"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("prop.properties") 的参数改成不存在的路径(如 "abc.properties"),运行观察返回的 inputStream 为 null、后续 pro.load(null) 抛空指针异常——体会"参数必须是 classpath 下真实存在的文件"app.name=J260713),在 Demo03 里用 pro.getProperty("app.name") 读取并打印——体会"改配置文件不用改代码"的配置与代码分离思想inputStream.close() 注释掉再运行,体会资源不关闭的隐患(结合 08-06/08-07 IO 流"用完必须 close"的铁律)——后面学 try-with-resources 可自动关闭com.mysql.cj.jdbc.Driver)和 db.url(jdbc:mysql://...)正是 JDBC 连接数据库的标配参数——思考后续学 JDBC 时,这套"类加载器读配置文件"的方式如何复用printUtil(Object obj) 方法注释"输出这个类的所有属性名--就必须要使用反射来实现"——思考为什么普通方式做不到(编译期不知道传入对象的具体类型)course/Demo05,观察三个判断 clazz1==clazz2 / clazz2==clazz3 / clazz3==clazz1 全部输出 true——印证"同一个类三种方式拿到同一个 Class 对象";再观察最后输出"人类的静态代码块"——印证 Class.forName 触发静态代码块int.class == Integer.TYPE 观察输出 true——体会"基本类型也有 Class 对象"(回顾 07-29 包装类 Integer.TYPE 知识)int[].class.getName() 观察输出 [I(JVM 中 int 数组的类型表示)——体会"数组也有 Class 对象"Person.class(注释掉 forName),重新运行观察"人类的静态代码块"不再打印——直观体会"类名.class 不触发静态代码块、forName 会触发";再用 new Person().getClass() 替换,观察也不打印(对象.getClass() 不触发)Class.forName("course.Student") 的类名改错(如 "course.StudentX"),运行观察抛 ClassNotFoundException——体会"受检异常必须要处理"(方法签名已声明 throws)clazz1.getName()(全类名 course.Student)/ getSimpleName()(Student)/ getSuperclass().getName()(java.lang.Object)/ getPackage().getName()(course)打印并理解每个方法的含义Class.forName() 既是获取 Class 对象的方式,也会触发类加载;本课"类加载机制 + 反射"两个主题通过 Class 对象紧密相连getConstructors()(所有 public 数组)/ getConstructor(Class...)(指定 public)/ getDeclaredConstructors()(所有含 private 数组)/ getDeclaredConstructor(Class...)(指定含 private)——背口诀:含 Declared 的能拿私有(包含所有),不含 Declared 的只能拿 publiccourse/Demo06,观察输出——getConstructors() 打印 public 构造数组、o instanceof Student 输出 true、无参构造创建的 name 为 null(study 输出 null在学习.....)、getDeclaredConstructors() 输出"构造方法个数:3"、暴力反射后"张三在学习....."——完整走一遍"获取构造方法→创建对象→强转调用方法"declaredConstructor.setAccessible(true) 一行注释掉再运行,观察抛 IllegalAccessException——直观体会"不 setAccessible(true) 无法访问 private 构造方法"、理解暴力反射为何需要放开权限studentClass.getConstructor(String.class, Integer.class)(不带 Declared)获取 private 2 参构造,观察抛 NoSuchMethodException——体会"不含 Declared 只能拿 public"的口诀getConstructor().newInstance()" 与 "普通 new Student()"——思考反射创建对象就是"运行时动态 new"(Spring IOC 根据配置字符串类名动态创建对象的原理)instanceof Student 判断后用 (Student) o 强转再调用 study()——回顾 07-24 多态向下转型知识getDeclaredConstructors().length 输出"构造方法个数:3"对照——体会 Declared 拿到的是"全部构造方法"throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException——理解反射 API 抛多个受检异常必须处理(回顾 07-28 异常处理:受检异常 vs 非受检异常)findClass());双亲委派模型保证核心类库只由启动类加载器加载,避免用户自定义同名类(如自己写个 java.lang.String)破坏 JDK 核心类Class.forName() / 类名.class / 对象.getClass() 三种获取 Class 对象的方式,都触发了类加载;反射正是建立在"加载后生成 Class 对象"这一机制之上java.lang.Class 类型的对象(类的"元信息":包名、方法、字段、父类等);Class.forName("全限定名") 可拿到它new 一个对象经历了类加载(首次)→ 分配内存 → 实例变量默认初始化 → 普通代码块 → 构造方法的完整链路——本课 Demo01 的"静态代码块 → 代码块 → 构造方法"正是这条链路的前半段直观演示jdk.internal.loader.ClassLoaders 类中定义——AppClassLoader(系统/应用)、PlatformClassLoader(平台)、BootstrapClassLoader(启动)内部类;通过 ClassLoader.getSystemClassLoader() 即可获取getParent() 返回 null 即代表"已到最顶层,没有父加载器"Class.getClassLoader() 返回加载该类的类加载器——自己写的类返回系统类加载器(AppClassLoader);核心类(java.lang.String / java.lang.Object)返回 null(BootstrapClassLoader 用 null 表示)——这是验证"哪些类由哪个加载器加载"最直接的手段DriverManager 是启动类加载器加载的核心类,但它需要加载用户 classpath 下的 MySQL/Oracle 驱动 jar(SystemClassLoader 加载的类);为此 JDK 提供了线程上下文类加载器(Thread Context ClassLoader),让核心类可以反过来使用子加载器加载驱动类——理解 SPI 机制是 Java 进阶的经典话题WebAppClassLoader 先加载自己 Web 应用的类、加载不到才委派给父加载器(与双亲委派顺序相反)——从而实现多个 Web 应用之间的类隔离(不同应用可用不同版本的 Spring 等),也支持热部署ClassLoader.getResourceAsStream("prop.properties") 会在 classpath(类路径) 下按给定路径查找资源文件——src/ 目录编译后就在 classpath 中,所以放在 src/ 根目录的文件能被找到;返回的 InputStream 就是文件的字节流com.mysql.cj.jdbc.Driver)、url(jdbc:mysql://主机:端口/库名?参数)、username(用户名)、password(密码)——正是 prop.properties 中配置的 4 项,后续学习 JDBC 时这套"配置文件读取参数"的方式会直接复用inputStream.close() 是手动关闭流;Java 7 提供了 try-with-resources 语法(try (InputStream in = ...) { }),实现 AutoCloseable 的资源会在 try 结束后自动关闭,无需手动 close——IO 流、网络连接等资源都推荐用它管理(后续课程会正式学习)Class.forName("com.mysql.cj.jdbc.Driver"))都建立在反射之上;理解反射 = 理解框架源码的第一把钥匙Class.forName() 既是"获取 Class 对象的三种方式"之一,也是第 3 节"类加载 6 种时机"中的第 ④ 条"反射创建 Class 对象"Class.forName("全类名") 默认会执行类的静态初始化块(初始化阶段);而 ClassLoader.loadClass("全类名") 默认不执行静态初始化(只完成加载+链接,不初始化)——这是 JDBC 驱动加载用 forName 的原因(驱动需要在注册时执行静态块)getDeclaredFields() / getDeclaredMethods() / getDeclaredConstructors() 获取私有(private)成员;配合 setAccessible(true) 可访问私有字段/调用私有方法——这正是"反射可能破坏封装性"的具体体现setAccessible(true) 可以绕过 Java 的访问控制(private 也能访问),这既是反射强大的原因,也是"破坏封装性"的根源——在某些安全要求高的场景(如模块化系统 JPMS)会限制反射的使用getDeclaredFields() / getDeclaredMethods() 获取私有字段与私有方法,套路与 Constructor 完全一致(Declared 拿私有 + setAccessible(true) 放开权限)Class.forName("com.xxx.UserService") + getDeclaredConstructor().newInstance())——这正是本课"反射是框架基础"论断的经典落地场景;理解反射构造方法 = 理解 Spring Bean 创建的第一步Class.newInstance() 被标记废弃(只能调用无参构造且权限检查严格),推荐使用 Constructor.newInstance()(可以传参、可以访问 private 构造)——本课 Demo06 正是后者setAccessible(true) 本质是绕过 Java 访问控制检查(private 也能访问),在模块化系统(JPMS)中受 --add-opens 约束;反射框架(如 Spring)大量使用它来注入私有字段——这是"反射破坏封装性"与"框架必须用反射"的辩证关系getDeclaredConstructor 只调用一次,后续复用 newInstance)来摊薄开销——这也是框架"用了反射但性能不差"的优化手段java.lang.ClassLoader(类加载器核心 API,含 getResourceAsStream)、java.lang.Class(Class 对象:getName/getSimpleName/getSuperclass/getPackage 等)、java.lang.reflect.Constructor(构造方法反射:getConstructors/getDeclaredConstructors/newInstance/setAccessible)、java.lang.reflect.Field(字段反射)、java.lang.reflect.Method(方法反射)、java.util.Properties(配置文件解析类,父类 Hashtable)、Class.forName(反射加载类)