
Java基础-注解&反射
目录
- 注解体系(内置注解 / 元注解 / 自定义注解)
- 反射核心概念、作用、优缺点
- Class 对象详解(特性、7 大类拥有 Class 对象、4 种获取方式)
- 反射核心 API:Constructor/Method/Field、setAccessible
- 类加载完整流程(联动 JVM,加载 / 连接 / 初始化、符号引用 & 直接引用)
- 静态加载 vs 动态加载
- 反射实操:配置文件动态创建对象
- 实战:自定义注解 + 反射解析(开发高频场景)
- 反射性能问题与优化方案
- 提问和回答
一、注解 Annotation
1. 注解作用
- 对代码做附加说明,本身不执行逻辑;
- 可被编译器、JVM、框架(Spring/MyBatis)通过反射读取元数据;
- 标注在包、类、方法、成员变量、参数等位置。
2. JDK 内置三大注解
| 注解 | 作用 |
|---|---|
| @Override | 校验当前方法是否重写父类方法,编译报错提示 |
| @Deprecated | 标记方法 / 类已废弃,调用时编译器警告 |
| @SuppressWarnings | 压制编译警告,常用参数:all、unchecked、deprecation |
3. 四大元注解(修饰其他注解)
元注解用于约束自定义注解的生命周期、使用范围
-
@Target
限定注解能标注的位置(
ElementType枚举):类、方法、字段、参数等 -
@Retention
(生命周期,优先级:SOURCE < CLASS < RUNTIME)
SOURCE:源码阶段保留,编译后丢弃,仅编译器可见CLASS:字节码中保留,类加载时丢弃,反射无法读取(默认)RUNTIME:运行时保留,反射可读取(框架自定义注解必用)
-
@Documented:生成 javadoc 文档时保留该注解
-
@Inherited:子类自动继承父类上的该注解
4. 自定义注解语法
- 使用
@interface声明注解,自动隐式继承Annotation接口; - 注解内的方法 = 配置参数,返回值仅支持:基本类型、String、Class、枚举、注解;
default给参数设置默认值;- 若只有一个参数,规范命名为
value,使用时可省略参数名。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldDesc {
// 字段描述,默认空字符串
String value() default "";
// 是否必填,默认false
boolean required() default false;
}
二、反射机制基础
1. 什么是反射
Java 静态语言,正常编码编译期确定所有类、方法;
反射:运行时通过类的 Class 对象,动态获取类全部结构、创建对象、调用方法、读写私有属性、读取注解,赋予 Java 动态语言特性。
核心底层:类加载后,JVM 在堆中生成唯一Class对象,该对象持有方法区中类的全部元数据(字段、方法、构造器、注解),反射所有操作都依托Class对象完成。
> 类比 Spring:Class 对象 ≈ BeanDefinition,存储类元信息;通过 Class 创建 Object ≈ Spring 根据 BeanDefinition 实例化 Bean。
2. 反射能实现的功能
- 运行时判断任意对象所属类;
- 运行时构造任意类的实例;
- 运行时获取类所有字段、构造器、方法(包含私有);
- 运行时调用任意对象的方法、读写字段;
- 运行时读取类 / 方法 / 字段上的注解;
- 生成动态代理(Spring AOP 底层)。
3. 反射优缺点
优点
- 动态解耦,运行时根据配置创建类,无需编译期硬编码依赖;
- 框架底层核心:Spring IoC、MyBatis、JUnit 全部依赖反射实现。
缺点
- 性能差:反射属于解释型操作,JVM 无法做编译优化,比直接 new / 调用慢很多;
- 破坏封装:
setAccessible(true)可强制访问私有变量、私有方法; - 代码可读性差,异常处理繁琐。
4. Class 类核心特性
- Class 本身也是 Java 类,继承
Object; - Class 对象由 JVM 类加载器自动创建,不能手动 new;
- 一个类在 JVM 中仅存在唯一一份 Class 对象(类只会加载一次);
- Class 对象存于堆;类的字节码元数据(方法、变量、常量池)存于方法区;
- 所有实例对象内部持有对应 Class 对象的引用,可反向获取类信息。
5. 哪些类型拥有 Class 对象
- 普通外部类、所有内部类(成员 / 局部 / 匿名)
- interface 接口
- 数组(不同维度 / 类型数组 Class 对象不同)
- enum 枚举
- annotation 注解
- 8 大基本数据类型
- void
6. 四种获取 Class 对象的方式
// 1. Class.forName("全类名") 【框架/配置文件最常用】
// 适用:只知道类字符串,运行时动态加载,抛出ClassNotFoundException
Class<?> cls1 = Class.forName("com.kaguya.demo.Cat");
// 2. 类名.class 【参数传递、性能最高】
// 编译期校验类存在,无异常,无需实例化
Class<Cat> cls2 = Cat.class;
// 3. 对象实例.getClass() 【已有对象时使用】
Cat cat = new Cat();
Class<? extends Cat> cls3 = cat.getClass();
// 4. 类加载器loadClass
ClassLoader loader = cat.getClass().getClassLoader();
Class<?> cls4 = loader.loadClass("com.kaguya.demo.Cat");
// 补充:基本数据类型 & 包装类
Class<Integer> intCls = int.class;
Class<Integer> integerCls = Integer.TYPE;
三、类加载(联动 JVM,反射底层依赖)
1. 静态加载 vs 动态加载
- 静态加载:编译期直接加载类,找不到类直接编译报错;new 创建对象、调用静态变量 / 方法、子类加载父类都属于静态加载;依赖性强。
- 动态加载:运行时才加载类,不用则不会报错;
Class.forName()反射属于动态加载,降低代码耦合。
2. 类加载完整 7 阶段
加载 → 连接(验证→准备→解析)→ 初始化 → 使用 → 卸载
1)加载 Loading
- 通过全限定名读取 class 二进制字节流(文件、Jar、网络);
- 将字节流转为方法区的运行时元数据;
- 在堆中生成该类唯一
Class对象,作为访问方法区元数据的入口。
2)连接 Linking
① 验证 Verification
校验 class 文件格式、元数据、字节码、符号引用合法性,防止恶意字节码破坏 JVM。
② 准备 Preparation
给类静态变量分配内存,赋默认初始值(0、null、false);常量 static final 直接赋值。
③ 解析 Resolution
把常量池中的符号引用替换为直接引用。
-
符号引用:仅文字描述目标(类名、方法名),无实际内存地址;
-
直接引用:指针 / 内存偏移量,可直接定位目标内存地址。
通俗理解:符号引用 = 只知道人名,直接引用 = 拿到住址能直接找到人。
3)初始化 Initialization
真正执行 Java 代码,给静态变量赋予自定义值、执行静态代码块:
- 编译器自动生成
<clinit>()方法,按代码顺序合并所有静态赋值、静态代码块; - JVM 多线程同步锁保证
<clinit>()仅执行一次,单线程完成类初始化。
4)使用 Using:正常 new 对象、调用方法
5)卸载 Unloading:GC 回收无引用的 Class 对象
四、反射核心操作 API
1. 通用开关:setAccessible (true)
Constructor、Method、Field 都继承AccessibleObject,拥有该方法:
true:关闭访问权限校验,允许读写私有字段、调用私有方法 / 构造器,同时小幅提升反射执行效率;false:开启权限校验,私有成员调用直接抛异常。
2. 获取构造器 Constructor
Class<Cat> cls = Cat.class;
// 获取所有public构造器
Constructor<?>[] publicCons = cls.getConstructors();
// 获取全部构造器(包含private)
Constructor<?>[] allCons = cls.getDeclaredConstructors();
// 获取指定参数的私有构造器
Constructor<Cat> privateCon = cls.getDeclaredConstructor(String.class);
privateCon.setAccessible(true);
// 通过构造器创建实例
Cat cat = privateCon.newInstance("小猫");
3. 获取成员变量 Field
// 所有public字段
Field[] publicFields = cls.getFields();
// 全部字段(私有/保护/公有)
Field[] allFields = cls.getDeclaredFields();
Field nameField = cls.getDeclaredField("name");
nameField.setAccessible(true);
// 赋值
nameField.set(cat, "小狗");
// 取值
String name = (String) nameField.get(cat);
4. 获取方法 Method
// 全部public方法(含父类)
Method[] publicMethods = cls.getMethods();
// 当前类所有方法(不含父类)
Method[] allMethods = cls.getDeclaredMethods();
Method sayHi = cls.getDeclaredMethod("sayHi");
sayHi.setAccessible(true);
// 调用方法,参数1:实例对象,后续为方法入参
sayHi.invoke(cat);
5. 配置文件动态反射创建对象
path=com.kaguya.demo.Cat
method=sayHi
public class TestReflect {
public static void main(String[] args) throws Exception {
// 读取配置文件
FileInputStream is = new FileInputStream("src/re.properties");
Properties prop = new Properties();
prop.load(is);
String className = prop.getProperty("path");
String methodName = prop.getProperty("method");
// 反射加载类
Class<?> cls = Class.forName(className);
Object obj = cls.newInstance();
// 获取方法并执行
Method method = cls.getMethod(methodName);
method.invoke(obj);
}
}
Cat实体
public class Cat {
private String name = "蚌埠指挥官";
public void sayHi(){
System.out.println("hello thank you!!! --by "+name);
}
}
五、实战:自定义注解 + 反射解析(业务开发高频场景)
需求
自定义字段注解,标记实体字段描述、是否必填;通过反射遍历实体,自动校验必填字段、打印字段说明。
1. 自定义注解
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,反射读取
@Documented
public @interface FieldDesc {
// 字段中文描述
String value();
// 是否必填
boolean required() default false;
}
2. 实体类使用注解
public class User {
@FieldDesc(value = "用户ID", required = true)
private Long id;
@FieldDesc(value = "用户名", required = true)
private String username;
@FieldDesc(value = "备注", required = false)
private String remark;
// getter/setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
}
3.反射工具类:解析注解、校验必填
import java.lang.reflect.Field;
public class AnnotationReflectUtil {
/**
* 打印实体字段说明,校验必填字段是否为空
*/
public static void checkRequired(Object obj) throws Exception {
Class<?> cls = obj.getClass();
// 获取所有字段
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
// 开启访问权限
field.setAccessible(true);
// 获取字段上的自定义注解
FieldDesc descAnno = field.getAnnotation(FieldDesc.class);
if (descAnno == null) continue;
// 打印字段说明
System.out.println("字段[" + field.getName() + "]:" + descAnno.value());
// 必填校验
if (descAnno.required()) {
Object value = field.get(obj);
if (value == null || "".equals(value.toString())) {
throw new RuntimeException("字段【" + descAnno.value() + "】不能为空!");
}
}
}
}
}
4.测试运行
public class TestAnnotationReflect {
public static void main(String[] args) throws Exception {
User user = new User();
user.setId(null); // 必填字段为空,触发异常
user.setUsername("张三");
AnnotationReflectUtil.checkRequired(user);
}
}
场景延伸
MyBatis 映射、参数校验框架、Excel 导入导出框架、接口文档生成,底层全部是「自定义注解 + 反射解析」这套逻辑。
提问与回答
-
什么是反射?反射有什么优缺点?
答:反射是 Java 在运行时通过 Class 对象动态获取类结构、创建对象、调用方法、读取注解的机制,赋予 Java 动态特性。
优点:
① 实现代码解耦,可通过配置文件动态加载类,框架底层核心(Spring、MyBatis、AOP);
② 可操作私有属性、方法,拓展性强。
缺点:
① 性能差,属于解释执行,JVM 无法优化,比直接调用慢;
② 破坏封装,setAccessible (true) 绕过权限校验;
③ 代码繁琐,大量受检异常,可读性低。
-
Class 对象有几种获取方式?各自适用场景?
①
Class.forName("全类名"):框架、配置文件动态加载,只知道字符串类名时使用;②
类名.class:参数传递、工具方法,编译期校验,性能最优;③
对象.getClass():已有实例对象,反向获取类信息;④
类加载器.loadClass("全类名"):自定义类加载器场景。 -
一个类会生成多个 Class 对象吗?为什么?
不会。类加载器只会加载一个类一次,方法区只存一份类元数据,堆中仅生成唯一 Class 对象,全局单例。
-
setAccessible (true) 作用是什么?
① 关闭 Java 语法访问权限校验,允许反射操作 private 私有字段、构造、方法;
② 关闭权限检查后,小幅提升反射执行效率。
Constructor/Method/Field 都继承 AccessibleObject,都拥有该方法。
-
getDeclaredXXX 和 getXXX 方法区别?
- getXXX ():仅获取public修饰的成员,包含父类公有方法 / 字段;
- getDeclaredXXX ():仅获取当前类所有修饰符(public/private/protected),不包含父类任何成员。
-
JDK 四大元注解分别是什么,作用?
① @Target:限制注解能标注的位置(类、方法、字段、参数等);
② @Retention:控制注解生命周期:SOURCE (源码丢弃) < CLASS (字节码保留,加载丢弃) < RUNTIME (运行时保留,反射可读取);自定义框架注解必须用 RUNTIME;
③ @Documented:生成 javadoc 文档时保留注解信息;
④ @Inherited:子类自动继承父类上的该注解。
-
自定义注解的规则?
① 使用 @interface 声明,隐式继承 Annotation 接口;
② 注解内方法代表配置参数,返回值只能是基本类型、String、Class、枚举、注解;
③ default 可以设置参数默认值;
④ 只有一个参数时规范命名为 value,使用时可省略参数名。
-
注解本身能执行业务逻辑吗?实际开发注解怎么生效?
注解只是标记元数据,本身无任何执行逻辑;
框架底层依靠反射,运行时扫描类 / 字段上的注解,读取注解参数后执行对应业务逻辑(如 MyBatis 映射、参数校验)。
-
静态加载和动态加载区别?反射属于哪一种?
静态加载:编译期加载类,找不到类直接编译报错;new、调用静态变量、子类加载父类都是静态加载;耦合度高。
动态加载:运行时才加载类,不用则不会报错;Class.forName () 反射属于动态加载,降低代码耦合。
-
简述类加载完整流程,符号引用和直接引用区别?
七阶段:加载 → 连接 (验证 / 准备 / 解析) → 初始化 → 使用 → 卸载
符号引用:仅字符串描述目标(类名、方法名),无内存地址,编译期存在;
直接引用:内存指针 / 偏移量,可直接定位对象地址,解析阶段把符号引用替换为直接引用。
-
类加载准备阶段和初始化阶段区别?
准备:仅给 static 静态变量分配内存,赋默认零值(0/null/false);static final 常量直接赋值;
初始化:执行静态代码块、静态变量自定义赋值,编译器生成<clinit>() 方法,JVM 加锁保证仅执行一次。
-
Class 对象存在哪里?类字节码元数据存在哪里?
Class 对象存堆;类的字节码、常量池、方法元数据存方法区(元空间)。
-
Spring IoC 容器为什么大量使用反射?
Spring 读取 xml / 注解配置拿到类全限定名,通过 Class.forName 动态加载类,反射调用无参构造创建 Bean 实例;依赖注入时反射 setter / 构造器赋值,全程无需硬编码 new 对象,实现解耦。
-
反射性能差,高并发场景如何优化?
① 缓存 Method/Field/Constructor 反射对象,不要循环重复获取;
② 统一开启 setAccessible (true) 关闭权限校验;
③ 超高性能需求使用 ASM 字节码框架替代原生反射;
④ 避免在高频循环中频繁反射创建对象。
-
手写场景:如何通过反射读取实体自定义注解,并校验字段必填?
核心流程:
- 获取实体 Class 对象;
- getDeclaredFields 拿到所有字段;
- field.setAccessible(true);
- field.getAnnotation (自定义注解.class) 获取注解;
- 判断注解 required 属性,反射 field.get () 获取字段值,为空抛出异常;
-
newInstance () 无参构造过时,JDK 推荐什么方式反射创建对象?
Class.newInstance () 要求类必须有无参构造,且抛出大量受检异常,JDK9 废弃;
推荐:getDeclaredConstructor (参数类型).newInstance (参数值),支持有参 / 私有构造。
-
反射能不能操作 static 静态变量?
可以。Field.get (null)、Field.set (null, 值),第一个参数传 null 即可操作静态成员。
-
数组、基本数据类型有 Class 对象吗?
有。int.class、Integer.TYPE、String [].class 都能获取对应 Class 对象。
-
接口可以获取 Class 对象吗?能通过反射 new 接口实例吗?
接口可以通过接口名.class 获取 Class 对象;不能直接实例化接口,需反射它的实现类,或动态代理生成实现类。