Skip to content

第四部分-反射与注解


反射

反射是在运行时加载类,并允许以编程方式解剖类中的各种成分(成员变量、方法、构造器等)。

这种“解剖”的意义其实我们早就见过。

比如在 IDEA 里,当我们写 对象. 时,它会自动提示可用的成员变量和方法——IDE 就是通过反射机制解析类的结构,获取所有可访问的成员信息。

反射的第一步,是加载类并获取类的字节码,即得到 Class 对象。拿到它之后,就可以进一步:

  • 获取构造器 → Constructor 对象
  • 获取成员变量 → Field 对象
  • 获取成员方法 → Method 对象

万物皆对象,连“类”本身也可以被当作对象来操作。

反射让我们在运行时拿到类的“字节码”——Class 对象,再从中解剖出构造器 Constructor、成员变量 Field、成员方法 Method,并对它们进行读写或调用。

加载获取类

在反射的第一步,我们必须先获得表示某个类的 Class 对象。只有拿到它,才能继续解析类中的构造器、字段和方法。

Java 提供了三种方式来获取 Class 对象,这三种方式的结果是相同的——无论哪种方式获取到的都是同一个 Class 实例。

类名 .class

这是最直接的方式,在编译期就能确定类的类型。

java
Class c1 = Wolf.class;
System.out.println(c1);

输出类似:

class com.example.Wolf

这里的输出并不是内存地址,而是类的全限定名/全类名(包名 + 类名)。Class 类重写了 toString() 方法,所以我们能直接看到可读的信息。

Class.forName(全类名)

如果类名是运行时动态确定的(例如配置文件中写的),就不能用 .class 方式,这时用 Class.forName()

java
Class c2 = Class.forName("com.example.Wolf");
System.out.println(c2);

这种方式会触发类的加载过程,如果类中有静态代码块,也会被执行。

对象 .getClass()

当你已经有了某个对象实例,但不确定它的类型时,可以直接向它要 Class 对象。

java
Wolf alpha = new Wolf();

Class c3 = alpha.getClass();
System.out.println(c3);

这种方式在实际业务中很常见,因为有时候我们拿到的不是类名,而是一个现成的对象。

获取构造器对象

拿到 Class 之后,下一步通常是定位构造器并创建对象。构造器对应的类型是 Constructor,可以批量拿,也可以按参数类型精确定位。

Declared 的方法更常用,因为它能拿到非 public 的成员。

获取全部构造器

当你尚不确定需要哪一个签名时,可以先把构造器列表取出来,再做筛选。

  • getConstructors()只拿 public
java
Constructor[] getConstructors()

只返回 public 构造器,权限受限。

  • getDeclaredConstructors()存在就能拿
java
Constructor[] getDeclaredConstructors()

返回类中声明的所有构造器(包含 private / protected / 默认 / public),开发更常用。

遍历全部构造器:

java
Class c = com.example.Wolf.class;
Constructor[] cons = c.getDeclaredConstructors(); // 更常用

for (Constructor con : cons) {
    System.out.println(con.getName() + " | 参数个数:" + con.getParameterCount());
}

获取指定构造器

当已知目标构造器的参数列表,例如“无参”或“(String,int)”时,直接精确定位更高效。

  • getConstructor(…parameterTypes)—— 只拿 public
java
Constructor getConstructor(Class... parameterTypes)
  • getDeclaredConstructor(…parameterTypes)—— 存在就能拿
java
Constructor getDeclaredConstructor(Class... parameterTypes)

通过参数类型列表精确定位目标构造器;常用在你明确知道要哪一个的时候。

拿指定的构造器:

java
Class c = com.example.Wolf.class;

// 无参构造
Constructor con1 = c.getDeclaredConstructor();

// 有参构造(例如:String 名字, int 等级)
Constructor con2 = c.getDeclaredConstructor(String.class, int.class);

使用构造器创建对象

定位到构造器之后,就能通过它创建实例。无论其权限如何都可访问,非公开构造器只需要先关闭访问检查。

  • newInstance(…initargs) 创建实例
java
Object newInstance(Object... initargs)

调用该构造器完成对象初始化并返回。
如果构造器不是 public,需要关闭访问检查

  • setAccessible(true) 关闭访问检查、“暴力反射”
java
void setAccessible(boolean flag)

设为 true 后,可调用 private 等非公开构造器。

调用无参 + 私有有参构造:

java
// 1) 无参构造(public)
Object w1 = con1.newInstance();
System.out.println(w1);

// 2) 私有有参构造(需要关闭访问检查)
con2.setAccessible(true);
Object w2 = con2.newInstance("影牙", 5);
System.out.println(w2);

正是因为可以突破封装边界(在受控前提下访问非 public 构造器),反射在不少关键框架里才派上大用场。现在感受可能不强,先知道它能做到什么,别小看这点能力。

过去是我们去找构造器再 new;用反射时,换成在运行期让“构造器”主动完成实例化。这种对类元信息的“反向驱动”方式,就是反射的意义所在。

获取成员变量对象

拿到 Class 之后,除了构造器,另一类常用的元信息就是成员变量。获取到 Field 的目的依旧朴素:取值赋值。先能拿到,再谈操作。

同样的经验法则:Declared 的方法更常用,因为它不受可见性限制。

获取全部成员变量

当你还不清楚类里都有些什么字段时,先把列表拿出来,再决定要用哪个。

  • getFields() —— 只拿 public
java
Field[] getFields()
  • getDeclaredFields() —— 存在就能拿
java
Field[] getDeclaredFields()

示例:遍历全部字段,先看“都有谁”

java
Class c = com.example.Wolf.class;
Field[] fields = c.getDeclaredFields(); // 更常用
for (Field f : fields) {
    System.out.println(f.getType() + " => " + f.getName());
}

这一步的价值在于“摸清家底”,输出类型与名称,便于后续精准定位。

获取指定成员变量

当你已经知道要操作哪个字段(例如 namerank),直接按名称精确定位。

  • getField(String name) —— 只拿 public
java
Field getField(String name)
  • getDeclaredField(String name) —— 存在就能拿
java
Field getDeclaredField(String name)

例如定位到 name 字段:

java
Class c = com.example.Wolf.class;
Field fname = c.getDeclaredField("name");

给字段赋值 / 取值

反射到字段的目的还是为了给字段赋值取值。定位到 Field 的下一步,就是对对象实例进行读写。
public 字段在直接访问时会报错,同样需要先关闭访问检查。

  • 赋值:set(Object obj, Object value)
  • 取值:get(Object obj)
  • 关闭检查:setAccessible(true)

示例:给 name 赋值并读取

java
com.example.Wolf w = new com.example.Wolf();

Field fname = com.example.Wolf.class.getDeclaredField("name");
fname.setAccessible(true);        // 允许访问非 public 字段

fname.set(w, "影牙");            // 赋值
String name = (String) fname.get(w); // 取值
System.out.println(name);         // 影牙

这就具备了通过反射“解剖并操控对象状态”的能力

获取成员方法对象

在拿到 Class 后,成员方法对应 Method。目的仍然直接:定位方法 → 执行方法。先能找到,再谈调用。

同样的经验法则:带 Declared 的方法更常用(不受可见性限制)。

获取全部方法

不确定需要哪一个时,先取清单再筛选。

  • getDeclaredMethods() —— 存在就能拿(不含父类继承的方法)
java
Method[] getDeclaredMethods()

示例:遍历方法名、参数个数、返回类型

java
Class c = com.example.Wolf.class;
Method[] methods = c.getDeclaredMethods();
for (Method m : methods) {
    System.out.println(m.getName() + " | 参数:" + m.getParameterCount()
            + " | 返回:" + m.getReturnType().getSimpleName());
}

需要包含父类 public 方法时,可用 getMethods()

获取指定方法

方法可以重载,因此除了方法名,还必须提供参数类型列表

  • getDeclaredMethod(String name, Class... parameterTypes) —— 存在就能拿
java
Method getDeclaredMethod(String name, Class... parameterTypes)

示例:无参方法与有参方法

java
Class c = com.example.Wolf.class;

// 无参:例如 howl()
Method howl = c.getDeclaredMethod("howl");

// 有参:例如 hunt(String target, int times)
Method hunt = c.getDeclaredMethod("hunt", String.class, int.class);

调用方法

定位到 Method 的最终目的是执行。非 public 方法同样需要先关闭访问检查。

  • 执行:invoke(Object obj, Object... args)
  • 关闭检查:setAccessible(true)

示例:调用无参 + 私有有参方法

java
com.example.Wolf w = new com.example.Wolf();

// 1) 无参(public)
Object r1 = howl.invoke(w);
System.out.println(r1);

// 2) 私有有参(需要关闭访问检查)
hunt.setAccessible(true);
Object r2 = hunt.invoke(w, "寒原野鹿", 2);
System.out.println(r2);

返回值类型不为 void 时,可按需要做强制类型转换;静态方法可将 obj 位置传 null 调用。
至此,成员方法的“拿到—定位—执行”链路与构造器、字段保持一致,形成完整的反射操作闭环。

注解

注解(Annotation)是写在代码里的结构化标记,由工具或框架在编译期或运行期读取并据此改变行为。
常见如 @Override(编译器做一致性检查)、@Test(测试运行器只执行被标记的方法)。

这些标记之所以“能被理解”,是因为注解把“说明书”写进了代码里,程序就能按说明执行。

自定义注解

基本语法与接口相似,但关键字是 @interface,属性以“无参方法”形式声明,可选 default 默认值。

java
public @interface MyTest {
    String name();
    double price() default 100.0;
    String[] authors();
}

有了注解的定义,接下来就是把它贴到类、字段或方法上,给程序“加说明”。

注解可以标在类、字段、方法等位置,属性以“键=值”形式赋值。

java
@MyTest(name = "群猎流程", price = 9.9, authors = {"影牙", "夜哨"})
public class WolfGuide {

    @MyTest(name = "编号", authors = {"雪爪"})
    private String code;

    @MyTest(name = "起猎", authors = {"黑焰", "裂风"})
    public static void main(String[] args) {
        // ...
    }
}

value是一个特殊属性名,当注解只有一个名为 value 的属性时,使用时可以省略 value=

java
public @interface Route {
    String value();
}

以下两种写法等价:

java
@Route("hunt/start")
// @Route(value = "hunt/start")
class WolfTask { }

声明与使用之外,还需要知道“注解在语言层面是什么”,这决定了它能被如何解析与调用。

注解的本质

从语义上讲,注解像一份接口式的“元信息描述”。反编译视角可见其等价的接口形态:所有注解类型都继承 java.lang.annotation.Annotation

java
public @interface MyMark {
    String id();
    boolean active();
    String[] tags();
}

// 反编译等价视角
public interface MyMark extends java.lang.annotation.Annotation {
    String id();
    boolean active();
    String[] tags();
}

使用时:

java
@MyMark(id = "W-01", active = true, tags = {"howl", "hunt"})
public void runPlan() { }

元注解

元注解是修饰注解的注解,用来回答两个问题:

  • 它能贴在哪里(@Target
  • 它能活到什么时候(@Retention

最小示例(先感受一下效果):

java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest { }

这表示:@MyTest 只能贴在方法上,并且会一直保留到运行期,便于反射解析。

常用的也主要是这两个元注解。

@Target-限定位置

不希望注解“到处乱贴”时,就用 @Target 指定作用位置。常见取值:

@Target 取值可标注位置说明
ElementType.TYPE类 / 接口声明在类型上
ElementType.FIELD成员变量包括静态/实例字段
ElementType.METHOD成员方法普通方法、非构造器
ElementType.PARAMETER方法参数形参位置
ElementType.CONSTRUCTOR构造器构造方法
ElementType.LOCAL_VARIABLE局部变量方法体内的本地变量

例如只想允许标注在方法上:

java
import java.lang.annotation.*;

@Target(ElementType.METHOD)
public @interface Route { }

@Retention-限定生命周期

@Retention 取值保留阶段编译后是否存在运行期是否可反射典型场景
SOURCE仅源码编译器提示、代码生成标记
CLASS(默认)源码 → 字节码框架不解析、仅保留到 .class
RUNTIME源码 → 字节码 → 运行期运行期反射解析(常用)

希望运行期还能解析到注解时,必须使用 RUNTIME

java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface HuntSpec {
    String value();
}

应用示例(仅作演示,示例统一“狼”):

java
@HuntSpec("寒原")
public class Wolf {

    @HuntSpec("月夜速猎")
    public void hunt() { }
}

例如 junit 的 @Test 注解,测试框架需要在运行期发现哪些方法要执行,因此它的注解通常是:

java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }

设置为 RUNTIME + METHOD,运行器才能在运行期扫描到并执行这些测试方法。

解析注解

解析注解(Annotation Processing)能够判断类、方法、字段、构造器上是否存在某个注解,并把注解里的属性值读取出来。

核心思路很直接:

要解析谁的注解,就先拿到谁的反射对象,再从它身上读取注解。

ClassMethodFieldConstructor 都实现了 AnnotatedElement 接口,提供了解析注解的常用方法:

java
Annotation[] getDeclaredAnnotations()                  // 取“本体声明”的所有注解
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) // 取指定注解
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)  // 是否存在某注解

通常先用 isAnnotationPresent 试探是否存在这个注解,再用 getDeclaredAnnotation 取对象,最后读属性值即可。

  1. 定义注解(含位置与生命周期)

目标:定义 @MyTest4,用于类与方法,在运行期可被解析;属性为 valueaaa(默认 100)、bbb(数组)。

java
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest4 {
    String value();
    double aaa() default 100;
    String[] bbb();
}
  1. 使用注解(贴到类与方法上)

演示时统一用“狼”的语境,但仅在代码内出现。

java
@MyTest4(value = "巡猎计划", bbb = {"影牙", "夜哨"})
class Demo {

    @MyTest4(value = "夜行速猎", aaa = 999, bbb = {"黑焰", "裂风", "雪爪"})
    public void test1() { }
}
  1. 解析类与方法上的注解

步骤:先拿反射对象 → 判断是否存在 → 获取注解对象 → 读取属性值

java
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

public class AnnotationParser {

    // 解析类上的注解
    public static void parseClass() throws Exception {
        Class<?> c = Demo.class;

        if (c.isAnnotationPresent(MyTest4.class)) {
            MyTest4 a = c.getDeclaredAnnotation(MyTest4.class);
            System.out.println(a.value());
            System.out.println(a.aaa());
            System.out.println(Arrays.toString(a.bbb()));
        }
    }

    // 解析方法上的注解
    public static void parseMethod() throws Exception {
        Method m = Demo.class.getDeclaredMethod("test1");

        if (m.isAnnotationPresent(MyTest4.class)) {
            MyTest4 a = m.getDeclaredAnnotation(MyTest4.class);
            System.out.println(a.value());
            System.out.println(a.aaa());
            System.out.println(Arrays.toString(a.bbb()));
        }
    }
}

两个解析函数分别演示了“解析类上的注解”和“解析方法上的注解”,结构一致,易于迁移到字段、构造器等位置。

评论