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

9.2 只是「看」说明书(拿类名、数方法),这一节开始「用」说明书:反射造对象、反射拆字段、反射调方法——三件套打完,"没有类名也能操作一个类"就落成代码了,Spring 的影子也出来了。

1. 引入:上节的伏笔,这节收

9.2 结尾埋了一句话:Field / Method / Constructor 是"反射三兄弟",先混脸熟。这节三兄弟全员上岗:

你想干的事 正常写法 反射写法
造一个对象 new Student("张三", 18) 构造器 newInstance("张三", 18)
读/改字段 s.name(还得是 public) Field 的 get(s) / set(s, 值)
调一个方法 s.eat("米饭") Method 的 invoke(s, "米饭")

左边那栏编译期就得写死类名;右边那栏——类名来自字符串,运行到一半才决定操作谁。这就是反射的全部意义。

2. 操作一:反射创建对象(最常用)

Class<?> c = Class.forName("Student");          // 拿说明书(9.1 三把钥匙)

Constructor<?> cons = c.getConstructor(String.class, int.class);
Student s = (Student) cons.newInstance("张三", 18);

三个要点:

  1. getConstructor(参数类型...) 传的是"类型",所以写 String.class、int.class——告诉它你要的是哪个构造器(重载定位,2.6 的账)。
  2. newInstance(实参) 才传"值",并且返回类型是 Object——因为 JVM 编译时不知道你要造谁,得你自己 (Student) 强转收回来(4.4 多态:编译看左,运行看右)。
  3. 无参构造可以偷懒:c.getDeclaredConstructor().newInstance()。
📝 为什么要强转?——就是 9.1 说的「失编译检查」现场

newInstance() 手里只有 Object,你强转错了类型,编译器不拦,运行时直接 ClassCastException(4.4 集齐的三大新手异常之一)。这是反射的代价,不是 bug。

3. 操作二:反射操作字段

Field f = c.getDeclaredField("name");   // 拿"字段说明书"(9.2 学的)
f.setAccessible(true);                  // 破 private 的后门

Object v = f.get(s);                    // 读:张三的 name
f.set(s, "波仔");                        // 写:改成"波仔"

两个要点:

  1. get / set 必须传对象——f 只是"字段说明书"(类级,每类一份),而字段的值长在每个对象身上。你要读张三还是李四的 name,就得把谁传进去。
  2. setAccessible(true):碰 private 字段前必须开,否则 IllegalAccessException。这叫暴力反射——3.3 封装立的规矩,反射手里有钥匙。

4. 操作三:反射调用方法

Method m = c.getDeclaredMethod("eat", String.class);  // 方法名 + 参数类型
m.setAccessible(true);                                // 私有方法才需要

Object result = m.invoke(s, "米饭");                   // 谁的方法 + 实参

和字段一个套路,注意两处参数长得像但不是一回事:

返回值也是 Object:方法有返回值就接住强转,没有就是 null。

5. 说明书 vs 对象:别混

(这是本节最容易懵的地方,看图↓ 看完回来继续)

所以反射 API 的规矩是:操作谁,就把谁传进去。f.get(s)、m.invoke(s, ...) 里的 s 缺不得。唯一例外是 newInstance()——它就是在"造对象",所以不用传。

6. 收网旧账

String className = "Student";              // 实际中来自配置文件/注解,反正是个字符串
Class<?> c = Class.forName(className);     // ① 字符串 → 说明书
Object obj = c.getDeclaredConstructor().newInstance();  // ② 说明书 → 对象
// ③ 后续:从 obj 拆字段、调方法、贴注解——框架初始化就这么三步

7. 动手:Demo03 反射三件套全流程

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Demo03 {
    public static void main(String[] args) throws Exception {
        Class<?> c = Class.forName("Student");

        // 一、造对象
        Constructor<?> cons = c.getConstructor(String.class, int.class);
        Object s = cons.newInstance("张三", 18);
        System.out.println(s);                       // 打印走 4.5 的 toString

        // 二、拆字段:连 private 都不放过
        Field f = c.getDeclaredField("name");
        f.setAccessible(true);
        System.out.println("改前:" + f.get(s));
        f.set(s, "波仔");
        System.out.println("改后:" + f.get(s));

        // 三、调方法:私有方法也照调
        Method m = c.getDeclaredMethod("eat", String.class);
        m.setAccessible(true);
        m.invoke(s, "米饭");
    }
}

三个 30 秒实验:

  1. 把 (Student) 强转删掉,用 Object 接住 newInstance 的结果再调 f.get(s)——照常能跑?想想为什么(f.get 参数本来就是 Object)。
  2. 注释掉 f.setAccessible(true) 再跑——IllegalAccessException 当场报出来,看它是不是运行时异常(5.2 家谱定位一下)。
  3. 把 getConstructor(String.class, int.class) 改成 getConstructor() 跑一把——NoSuchMethodException:你没写无参构造,反射找得比编译器还较真。

8. 常见误区

⚠️ 五个坑
  1. ❌ newInstance() 直接当 Student 用 —— 返回的是 Object,不强转后面全报"找不到成员"。先 (Student) 再说。
  2. ❌ 拿私有字段忘开 setAccessible(true) —— getDeclaredField 只负责"找到","碰"之前还得开后门,缺一报 IllegalAccessException。
  3. ❌ f.get() 忘传对象 —— Field 是说明书不是值,不传对象它不知道读谁;错写成 f.get() 直接编译不过,算友好提醒。
  4. ❌ getMethod 和 invoke 的参数搞混 —— 前者传"类型"(String.class),后者传"值"("米饭")。记口诀:先看长相,再干活。
  5. ❌ 反射万能论,处处用反射 —— 反射慢、丢编译检查、破坏封装。框架底层用是逼不得已,你的业务代码能 new 就 new。

9. 可折叠自测

❓ Q1:`getConstructor(String.class, int.class)` 里的 `String.class` 是什么?为什么要传它?

它是 String 这个类的 Class 对象(9.1 三把钥匙之一的"类名.class"写法)。构造器有重载,反射靠"参数类型列表"精确定位你要哪一个——和 2.6 重载定位一个道理。

❓ Q2:`f.get(s)` 为什么要传 `s`?不传会怎样?

f 只是字段说明书(类级,一份),值存在每个对象身上(对象级,一人一份)。不传对象编译都过不了——它不知道该读谁的 name。

❓ Q3:`getMethod("eat", String.class)` 和 `m.invoke(s, "米饭")`,两处的参数有什么本质区别?

前者传类型(用于在重载中定位方法,看的是"长相");后者传值(真正干活时的实参)外加"调用谁的方法"。口诀:先看长相,再干活。

❓ Q4:`setAccessible(true)` 干了什么?哪两处会用到它?它证明了什么旧账?

打破 private 门禁(暴力反射)。私有字段 f.get/set 前、私有方法 m.invoke 前都要开。它证明 3.3 说的"封装防的是编译期误用",运行期反射手里有钥匙——所以反射也要守规矩,别乱摸。

❓ Q5(毕业题):不看笔记,写出"配置式工厂":给定一个字符串类名,不出现 `new`,造出对象并把它所有 private 字段打印成 `字段名 = 值`。
Class<?> c = Class.forName(className);                       // 字符串 → 说明书
Object obj = c.getDeclaredConstructor().newInstance();       // 说明书 → 对象
for (Field f : c.getDeclaredFields()) {                      // 全字段(含私有)
    f.setAccessible(true);                                   // 开后门
    System.out.println(f.getName() + " = " + f.get(obj));    // 传对象读值
}

三步写全,你就把 9.1 毕业题的 Spring 雏形补完最后一环了。

10. 下一步学什么

相关笔记