☕ 第九章 反射机制和注解
📄 一句话概括

正着走是「照图纸造对象」(编译期一切已知),反射是拿着对象反查图纸——运行时拿到 Class 对象,类的字段、方法、构造方法全能拆开看,框架的命根子。


1. 为什么需要反射:new 有个致命前提

先看正常的写法(4.2 到 8 章一直在用的):

Person p = new Person();   // 编译期就认识 Person,一切尽在掌握
p.study();                 // 方法名拼错?编译器当场红叉

new 的前提是:类名必须提前写死在代码里。 这在前九章毫无问题,但有一种场景它会卡死:

程序启动时读一个配置文件,文件里写着 personClass=com.demo.Person——类名是运行时才拿到的字符串。你怎么写 new ?字符串可不能直接 new。

String className = 配置文件里读出来的;   // "com.demo.Person"
??? obj = new className();              // ✗ 编译报错,字符串不是类名

这就是反射出场的地方——运行时才拿到类名,也能把这个类拆开、用起来。

📝 框架的命根子

Spring 的 @Component 自动装配、MyBatis 的 Mapper、JUnit 跑你写的测试类……全是「配置/注解里给个类名字符串,框架运行时反射造对象、调方法」。学反射不是炫技,是看懂框架的前提。

2. 反射的核心角色:Class 对象(类的说明书)

Java 里每个类被加载进 JVM 时,JVM 会给它造一份唯一的「类说明书」——一个 Class 类型的对象:

⚠️ 别混淆:Class 对象 ≠ 普通对象

new Person() 造的是人;Person.class 拿到的是关于 Person 这个类的说明书。前者是一万个独立个体,后者全类只有一份。Class 本身也是一个类(java.lang 包,不用 import),但它描述的是「类」而不是「东西」。

3. 三把钥匙:获取 Class 对象

同一个类,三条路拿到的是同一个 Class 对象(用 == 比全是 true):

方式 写法 适用场景 备注
① Class.forName Class.forName("com.demo.Person") 类名是运行时才拿到的字符串(配置文件、框架)——反射主用法 参数是全类名(带包名);抛受检异常 ClassNotFoundException
② 类名.class Person.class 编译期就知道类名;当参数传「类型说明书」 最安全,不抛异常
③ 对象.getClass() p.getClass() 手里已有对象,反查它的出身 Object 的原生方法,人人自带

三种方式的验证:

Class<?> c1 = Class.forName("com.demo.Person");  // 字符串路
Class<?> c2 = Person.class;                       // 类名路
Person p = new Person();
Class<?> c3 = p.getClass();                       // 对象路

System.out.println(c1 == c2);   // true
System.out.println(c2 == c3);   // true —— 同一份说明书
💡 5.2 的老朋友又来了

Class.forName 抛的 ClassNotFoundException 是受检异常——编译器当场逼你 throws 或 try-catch。这是 6.6 ParseException 之后,受检异常的又一次实战登场。类名字符串写错(少包名、拼错字)就是这异常最常见的来源。

4. 拿到说明书后,能干什么(本章后续逐个解锁)

Class<?> c = Class.forName("com.demo.Person");

System.out.println(c.getName());        // 全类名 com.demo.Person
System.out.println(c.getSimpleName());  // 短名字 Person

// 没有类名也能造对象(下一节的主角):
Object obj = c.getDeclaredConstructor().newInstance();

// 看字段、看方法、连 private 都能摸(后续几节):
c.getDeclaredFields();   // 所有字段
c.getDeclaredMethods();  // 所有方法

先别急着全记——本节只需要记住:一切从 Class 对象开始,三把钥匙选一把拿到它。造对象、拆字段、调方法是后续几节的正戏。

📝 呼应 8 章:类型擦除的另一半真相

8.1 说「泛型只活编译期,运行时都是裸的」——那运行时想拿类型信息怎么办?答案就是反射:Class 对象是运行时唯一能摸到类型信息的地方。泛型管编译期安检,反射管运行时拆解,一正一反凑成完整的类型世界。

5. 反射的代价与规矩

代价/规矩 说明
慢 反射调用比直接调用慢(编译器没法优化),框架内部有大量优化,业务代码别拿反射当常规手段
破坏封装 连 private 都能摸——这是「后门」不是「正门」,工具和框架用它,你的业务代码不该用
失去编译期检查 方法名字符串写错了,编译器不管,运行时才炸(5.1 三类错误里最晚暴露的那种)

一句话:反射是给框架和工具用的手术刀,不是给业务代码用的螺丝刀。

6. 动手环节

Demo01:三把钥匙与说明书

package com.demo;

// 1. 准备一个类
class Person {
    private String name;

    public Person() {
        System.out.println("Person 空参构造执行了");
    }

    public void study() {
        System.out.println(name + " 在学习");
    }
}

public class Demo01 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 2. 三把钥匙
        Class<?> c1 = Class.forName("com.demo.Person");  // ① 字符串路(注意全类名)
        Class<?> c2 = Person.class;                       // ② 类名路
        Class<?> c3 = new Person().getClass();            // ③ 对象路

        // 3. 验证:同一份说明书
        System.out.println(c1 == c2);   // true
        System.out.println(c2 == c3);   // true

        // 4. 说明书上的基本信息
        System.out.println(c1.getName());        // com.demo.Person
        System.out.println(c1.getSimpleName());  // Person
    }
}

实验 1(30 秒):把 Class.forName("com.demo.Person") 的参数改成 "Person"(不带包名)再跑——亲眼见 ClassNotFoundException,记住全类名三字。

实验 2(30 秒):在 main 里连续 new Person() 三次,分别 getClass() 后用 == 两两比较——三次都是 true,体会「对象可以有无数个,说明书只有一份」。

实验 3(30 秒):打印 int.class 和 String.class——基本类型也有说明书(int 的 Class 是 int,不是 Integer),以后拆 ArrayList<Integer> 的泛型时会再遇到。

7. 常见误区

⚠️ 误区 1:Class 对象和 new 出来的对象是一回事

不是。Person.class 是类的说明书(全类一份);new Person() 造的是具体的人(随便多少个)。反射先拿说明书,再决定造不造人。

⚠️ 误区 2:forName 里的类名不写包名

Class.forName("Person") 直接炸 ClassNotFoundException。参数必须是全类名(带包名),这是头号新手错。

⚠️ 误区 3:三把钥匙拿到的是三个不同的 Class

同一个类全 JVM 只有一份 Class 对象,三条路拿到的用 == 比全是 true(面试常考)。

⚠️ 误区 4:反射很快 / 反射想用就用

反射比直接调用慢一截,还绕过编译期检查。它是框架的底层手段,不是业务代码的常规姿势。

⚠️ 误区 5:反射能绕过封装 = 随便摸 private

能摸是能力,不是许可。业务代码里随意摸 private 会把 3.3 封装性辛苦建好的规矩全拆了。知道它「能」即可。

8. 自测题

❓ Q1:反射解决了 new 的什么死穴?

new 要求类名编译期写死;反射让「运行时才拿到的类名字符串」也能拆类、造对象、调方法。框架(Spring/JUnit/MyBatis)全靠这一手。

❓ Q2:三种获取 Class 的方式,哪种最常配配置文件用?为什么?

Class.forName(全类名)——因为只有它接收字符串,而配置文件里读出来的正是字符串;另外两种都要求编译期就知道类名。

❓ Q3:`Class.forName` 为什么编译器逼你处理异常?

它抛的 ClassNotFoundException 是受检异常(5.2):类名字符串是外部数据,编译器无法保证它正确,所以强制你写预案。和 6.6 parse 抛 ParseException 同款待遇。

❓ Q4:`new Person().getClass() == Person.class` 结果是什么?说明什么?

true。说明同一个类全 JVM 只有一份 Class 对象——说明书不是跟着对象走的,是跟着类走的。

❓ Q5:(毕业题)Spring 读到配置 `userService=com.demo.UserService` 后要创建这个对象,用普通 new 行不行?该怎么干?大概分几步?

不行——new 需要编译期类名,这里只有字符串。反射分三步:① Class.forName("com.demo.UserService") 拿说明书;② 用说明书的构造方法造对象(下一节);③ 按需调方法/填字段(后续几节)。这正是框架自动装配的最小雏形。

9. 下一步学什么

相关笔记