Java开发教程24-反射与注解

xianaldai 2026-08-27 01:06 1

本期是Java开发教程系列的第二十四期,本期介绍反射与注解.反射是"运行时解剖类"的能力,注解是"贴在代码上的标签".上一期 Gson 为什么能自动把你类的字段写进 JSON?这一期揭开谜底.若你已经熟悉 Class 对象、反射常用 API 和自定义注解,可以跳过本期内容.


天外音:想要的图在主贴最后了.


版权声明:图片部分来自于网络,如有侵权,可联系删除.


每期叠甲:

时间仓促,水平有限,文中难免存在缺点和错误,恳请广大开发者和从业者指正.




一、反射:运行时解剖一个类


平时我们写代码,类型都是编译期定死的:new Student() 就得是 Student,s.name 就得是 name.但有一类需求在编译期根本做不到:



  • 一个工具要处理任何类.不知道类的名字,等运行了才知道

  • 要遍历一个类的所有字段(Gson 就这么干的,第 23 期)

  • 框架要在你的代码跑之前检查你写没写注解

    这类需求的答案就是反射(Reflection):程序在运行期拿到类的结构信息(类名、字段、方法、构造器、注解),甚至能当场创建对象、调用方法:类对你不再神秘,像被解剖一样看得清清楚楚.


每个类都对应一个 Class 对象.这是反射的入口.拿到它的三种方式:


// 方式一:类名.class
Class<Student> c1 = Student.class;

// 方式二:对象的 getClass()
Student s = new Student("小明", 92);
Class<? extends Student> c2 = s.getClass();

// 方式三:字符串形式(类名写错编译期也发现不了,运行期才抛 ClassNotFoundException)
Class<?> c3 = Class.forName("Student");

三种方式拿到的是同一个对象.Student 类在 JVM 里只有一个 Class 实例,这点和后面要讲的单例模式(全局只有一个实例)是同一个思想.方式三的字符串形式是框架的最爱:配置文件里写一个类名,程序运行期按名字加载,所以 Spring才能做到"不改代码换实现".




二、解剖三件套:字段、方法、构造器


拿到 Class 后,可以查它的成员:


Class<?> c = Student.class;

// 所有字段(不含继承来的),getName 是字段名,getType 是字段类型
for (java.lang.reflect.Field f : c.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}
// name : String
// score : double

// 所有方法
for (java.lang.reflect.Method m : c.getDeclaredMethods()) {
System.out.println(m.getName());
}

// 所有构造器
for (java.lang.reflect.Constructor<?> con : c.getDeclaredConstructors()) {
System.out.println(con);
}

对应关系记一张表:

































成员 查询 API 说明
字段 getDeclaredFields() getDeclaredXxx 只管本类声明的,不含父类
方法 getDeclaredMethods() 方法对象能拿到名字、参数、返回类型
构造器 getDeclaredConstructors() 拿到后能 newInstance 创建对象
注解 getDeclaredAnnotations() 反射读取注解,下一节的主角

当场创建对象、调用方法:


// 用无参构造器创建对象
Student s = c.getDeclaredConstructor().newInstance();

// 拿方法对象,invoke 调用
java.lang.reflect.Method setName = c.getDeclaredMethod("setName", String.class);
setName.invoke(s, "小红"); // 等价于 s.setName("小红")

invoke 的妙处:方法名可以是字符串:方法名存进配置文件、由用户输入决定调哪个方法,这种"运行时才知道调谁"的场景,只有反射能办.




三、setAccessible:突破 private


第 4 期学的访问控制(public/private),反射面前有例外:字段/方法对象上调用 setAccessible(true),private 也能摸:


java.lang.reflect.Field nameField = c.getDeclaredField("name");
nameField.setAccessible(true); // 解锁 private
nameField.set(s, "小刚"); // 直接改私有字段
System.out.println(nameField.get(s)); // 直接读私有字段

这看起来和"封装"(第 5 期)矛盾?两点说明:



  1. 封装是约定,反射是例外.private 是给普通代码看的规矩;框架、调试工具需要突破它,所以要留后门(我让你留测试用的接口或者实现,不是让你乱上生产环境的后门!!! !!! !!!)

  2. 能开不意味着该乱开.setAccessible(true) 有性能和安全隐患,业务代码里几乎不该出现;它是框架和工具的特权


顺带说一句,setAccessible 在 Java 17+ 对"非模块内反射"有严格限制(模块系统),跑框架内部用没问题;业务代码直接反射私有成员,在 JDK 17 下可能要加 --add-opens 启动参数.遇到报错先想到这句.




四、注解:贴在代码上的标签


注解(Annotation)你已经见了很多:@Override(第 6 期)、@FunctionalInterface(第 15 期)、@Test(下期 JUnit)、@SerializedName(第 23 期).它们是什么?


注解 = 贴在你的类/方法/字段上的标签.比如 @Override 的意思是"我这个方法覆盖了父类的方法".编译器读了这个标签,发现你根本没覆盖,就报错.


自定义一个注解:


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) // 关键!表示这个注解在运行期还活着,反射才读得到
public @interface NotNull {
String message() default "字段不能为空"; // 注解的"参数",带默认值
}

语法要点:



  • @interface 定义注解(读作"这是个注解类型")

  • @Retention(RUNTIME) 声明存活期:默认注解只活到编译期,反射读不到;标上 RUNTIME 才保留到运行期

  • 里面声明的"方法"就是注解的参数,用的时候 @NotNull(message = "名字不能空") 传值,不传用默认值

    注解本身什么都不做:它只是一张标签.让标签生效的是读者:编译器读 @Override,校验器读 @NotNull.而运行期注解的唯一读者就是反射:


NotNull a = field.getAnnotation(NotNull.class);
if (a != null) { // 这个字段贴了 @NotNull
Object value = field.get(obj);
if (value == null) {
throw new IllegalStateException(a.message());
}
}

看到没:反射拿到注解 → 读参数 → 决定行为."注解 + 反射"是 Java 框架的地基(JUnit、Spring、MyBatis 全靠这对组合),下一节亲手搭一个试试水.




五、小项目:迷你校验器


写一个注解 + 反射校验器:给任意对象的字段贴上 @NotNull@Range,一个 validate 方法通吃所有类.


文件 NotNull.java:


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
String message() default "字段不能为空";
}

文件 Range.java:


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
int min() default 0;
int max() default 100;
String message() default "数值超出范围";
}

文件 Validator.java:


import java.lang.reflect.Field;

public class Validator {

// 校验任意对象:反射遍历所有字段,按注解检查
public static void validate(Object obj) throws IllegalAccessException {
Class<?> c = obj.getClass();

for (Field f : c.getDeclaredFields()) {
f.setAccessible(true); // 私有字段也查
Object value = f.get(obj);

// 检查 @NotNull
NotNull notNull = f.getAnnotation(NotNull.class);
if (notNull != null && value == null) {
throw new IllegalStateException(notNull.message() + ":" + f.getName());
}

// 检查 @Range(只对数字生效)
Range range = f.getAnnotation(Range.class);
// 语法小补充:if (value instanceof Number num) 表示"value 真的是 Number 时,
// 顺便把它转成 Number 并绑定到变量 num 上",是 JDK 16 起的"模式匹配 instanceof",
// 省掉了旧写法 (Number) value 的强转.后面还会见到它.
if (range != null && value instanceof Number num) {
double d = num.doubleValue();
if (d < range.min() || d > range.max()) {
throw new IllegalStateException(range.message() + ":" + f.getName());
}
}
}
}
}

文件 Main.java:


public class Main {
// 给玩家类贴标签:名字不能空,等级 1~99
static class Player {
@NotNull(message = "名字不能为空")
String name;

@Range(min = 1, max = 99, message = "等级要在 1~99 之间")
int level;

@Range(min = 0, max = 20, message = "血量要在 0~20 之间")
double hp;
}

public static void main(String[] args) throws Exception {
Player p = new Player();
p.name = null;
p.level = 100;
p.hp = 20;

try {
Validator.validate(p);
System.out.println("校验通过");
} catch (IllegalStateException e) {
System.out.println("校验失败: " + e.getMessage());
}
}
}

运行结果:


校验失败: 名字不能为空:name

p.name = "Steve" 加上再跑,失败信息变成等级超范围;再把 level 改成 10,全部通过.体会:校验逻辑只写一次,贴了标签的任意类都能用.新增校验规则只是加一个注解,不用改 Validator.这就是框架们(Spring的参数校验)的雏形.




朝花夕拾:第 23 期的"魔法",今天是"反射"


第 23 期我们问:Gson 怎么知道 Player 有哪些字段、每个字段什么类型?答案就在本期:gson.toJson(obj) 内部大概就是:



  1. obj.getClass() 拿 Class 对象

  2. getDeclaredFields() 拿到所有字段(这就是为什么字段类型能自动写进 JSON)

  3. 对每个字段 f.get(obj) 取值,再按 f.getType() 写出对应类型

    第 23 期要求"类要有无参构造器"的谜底也在这:gson.fromJsongetDeclaredConstructor().newInstance() 创建对象,没有无参构造器就建不出来.




总结


本期学会了"运行时解剖"与"贴标签"两大能力:



  • 反射入口:Class 对象三种拿法(类名.class/对象.getClass()/Class.forName)

  • 解剖三件套:getDeclaredFields/getDeclaredMethods/getDeclaredConstructors;invoke 动态调方法

  • setAccessible:突破 private 的后门,框架特权,业务代码慎用

  • 注解:@interface 定义、@Retention(RUNTIME) 保活、参数带默认值;注解只是标签,反射读取才生效

  • 迷你校验器小项目:@NotNull/@Range + 一个通吃所有类的校验器

  • 反射绝对不可以滥用,会造成严重的bug和性能问题,某些AI在写我的世界模组就很喜欢滥用.


下一期我们学习单元测试 JUnit:今天学的注解,马上要被 JUnit 用来"发现你的测试方法".









哦天哪,这个图包我好不容易从143张找到几张能过审的

最新回复 (1)
  • Peach_Lu 08-27 03:44
    1

    佬友这么晚还更新呢 辛苦佬友了 ^-^

* 帖子来源Linux.do
返回