
Java基础:反射与注解 系统性知识体系总结
一、反射(Reflection)核心原理
1.1 反射的本质与定义
反射是Java语言提供的一种动态获取类信息和动态调用对象方法的机制。在运行状态中:
- 对于任意一个类,都能够知道这个类的所有属性和方法
- 对于任意一个对象,都能够调用它的任意一个方法和属性
- 这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制
核心思想:将类的各个组成部分(字段、方法、构造器、注解等)封装成一个个对象(Field、Method、Constructor、Annotation),通过这些对象在运行时操作类和对象。
1.2 反射的底层原理:Class类
- Java程序在编译后会生成
.class字节码文件 - JVM加载字节码文件时,会在堆内存中生成一个对应的
java.lang.Class对象 - 这个
Class对象包含了类的完整结构信息,是反射的入口 - 每个类在JVM中有且仅有一个
Class对象
1.3 获取Class对象的三种方式
| 方式 | 代码示例 | 适用场景 |
|---|---|---|
| 类名.class | Class<?> clazz=Person.class; |
编译期已知类名,最安全、性能最好 |
| 对象.getClass() | Person p=new Person(); Class<?> clazz=p.getClass(); |
已有对象实例时使用 |
| Class.forName() | Class<?> clazz=Class.forName("com.example.Person"); |
运行期动态加载类,如JDBC驱动加载 |
1.4 反射常用API
1.4.1 操作构造器
// 获取所有public构造器
Constructor<?>[] constructors=clazz.getConstructors();
// 获取指定参数的public构造器
Constructor<?> constructor=clazz.getConstructor(String.class, int.class);
// 获取所有构造器(包括private)
Constructor<?>[] declaredConstructors=clazz.getDeclaredConstructors();
// 获取指定参数的构造器(包括private)
Constructor<?> declaredConstructor=clazz.getDeclaredConstructor(String.class);
// 创建对象实例
Object obj=constructor.newInstance("张三", 20);
// 访问private构造器需要设置可访问性
declaredConstructor.setAccessible(true);
Object privateObj=declaredConstructor.newInstance("李四");
1.4.2 操作字段
// 获取所有public字段
Field[] fields=clazz.getFields();
// 获取指定名称的public字段
Field field=clazz.getField("name");
// 获取所有字段(包括private)
Field[] declaredFields=clazz.getDeclaredFields();
// 获取指定名称的字段(包括private)
Field declaredField=clazz.getDeclaredField("age");
// 获取字段值
Object value=field.get(obj);
// 设置字段值
field.set(obj, "王五");
// 访问private字段需要设置可访问性
declaredField.setAccessible(true);
declaredField.set(obj, 25);
1.4.3 操作方法
// 获取所有public方法(包括继承的)
Method[] methods=clazz.getMethods();
// 获取指定名称和参数的public方法
Method method=clazz.getMethod("sayHello", String.class);
// 获取所有本类声明的方法(包括private,不包括继承的)
Method[] declaredMethods=clazz.getDeclaredMethods();
// 获取指定名称和参数的方法(包括private)
Method declaredMethod=clazz.getDeclaredMethod("privateMethod");
// 调用方法
Object result=method.invoke(obj, "你好");
// 调用private方法需要设置可访问性
declaredMethod.setAccessible(true);
declaredMethod.invoke(obj);
1.5 反射的优缺点
优点:
- 动态性:运行时动态加载类、创建对象、调用方法,提高程序的灵活性和扩展性
- 通用性:可以编写通用代码处理不同类型的对象,如ORM框架、序列化框架
- 突破访问限制:可以访问类的私有成员(虽然不推荐,但在某些场景下必要)
缺点:
- 性能开销:反射操作需要解析字节码,比直接调用慢10-100倍
- 安全风险:可以访问私有成员,破坏了封装性和安全性
- 代码可读性差:反射代码比直接调用复杂,难以理解和维护
- 编译期无法检查:反射操作的错误只能在运行时发现,增加了调试难度
1.6 反射的典型应用场景
- 框架开发:Spring IoC容器、MyBatis ORM框架、Hibernate
- 序列化与反序列化:JSON解析(Jackson、Gson)
- 动态代理:JDK动态代理
- 单元测试:JUnit、TestNG
- 插件开发:Eclipse、IntelliJ IDEA插件
- 配置文件解析:读取配置文件中的类名并实例化
二、注解(Annotation)核心原理
2.1 注解的本质与定义
注解是Java 5引入的一种元数据,用于为代码(类、方法、字段、参数等)添加额外信息。这些信息可以在编译期、类加载期或运行期被读取和处理。
核心思想:注解本身不直接影响代码的执行,它只是一种标记,需要通过专门的工具(注解处理器)来解析和处理。
2.2 注解的分类
2.2.1 按来源分类
- JDK内置注解:
@Override、@Deprecated、@SuppressWarnings、@FunctionalInterface等 - 第三方注解:Spring的
@Controller、@Service、@Autowired,MyBatis的@Mapper等 - 自定义注解:开发者根据自己的需求定义的注解
2.2.2 按生命周期分类
| 生命周期 | 说明 | 适用场景 |
|---|---|---|
| SOURCE | 只保留在源码中,编译时被丢弃 | 编译期检查,如@Override |
| CLASS | 保留在字节码文件中,JVM加载时被丢弃 | 字节码级别的处理,如一些字节码增强工具 |
| RUNTIME | 保留在字节码文件中,JVM加载时读入内存,运行时可以通过反射获取 | 运行时动态处理,如Spring的依赖注入 |
2.3 元注解(Meta-Annotation)
元注解是用于修饰注解的注解,JDK提供了4个标准元注解:
2.3.1 @Target
指定注解可以应用的目标元素类型,取值来自java.lang.annotation.ElementType枚举:
TYPE:类、接口、枚举FIELD:字段METHOD:方法PARAMETER:方法参数CONSTRUCTOR:构造器LOCAL_VARIABLE:局部变量ANNOTATION_TYPE:注解PACKAGE:包
示例:
@Target({
ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation {
}
2.3.2 @Retention
指定注解的生命周期,取值来自java.lang.annotation.RetentionPolicy枚举:
RetentionPolicy.SOURCERetentionPolicy.CLASS(默认值)RetentionPolicy.RUNTIME
示例:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
2.3.3 @Documented
指定被修饰的注解会被javadoc工具提取到API文档中。
2.3.4 @Inherited
指定被修饰的注解具有继承性。如果一个类使用了被@Inherited修饰的注解,那么它的子类会自动继承这个注解。
注意:@Inherited只对类级别的注解有效,对方法和字段级别的注解无效。
三、自定义注解
3.1 自定义注解的语法
[元注解]
public @interface 注解名称 {
// 注解元素(属性)
类型 元素名称() [default 默认值];
}
3.2 注解元素的类型限制
注解元素的类型只能是以下几种:
- 基本数据类型(byte、short、int、long、float、double、char、boolean)
- String
- Class
- 枚举
- 注解
- 以上类型的数组
3.3 自定义注解示例
import java.lang.annotation.*;
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnnotation {
// 字符串类型元素
String value() default "";
// 整数类型元素
int age() default 18;
// 枚举类型元素
Gender gender() default Gender.MALE;
// 数组类型元素
String[] hobbies() default {
};
// 注解类型元素
OtherAnnotation other() default @OtherAnnotation;
}
// 枚举类型
enum Gender {
MALE, FEMALE
}
// 其他注解
@interface OtherAnnotation {
String name() default "other";
}
3.4 注解的使用
// 使用注解,指定元素值
@MyAnnotation(value="张三", age=20, gender=Gender.MALE, hobbies={
"读书", "跑步"})
public class Person {
@MyAnnotation("sayHello方法")
public void sayHello() {
System.out.println("Hello");
}
}
特殊说明:
- 如果注解只有一个名为
value的元素,使用时可以省略元素名,直接写值 - 如果元素是数组类型,且只有一个值,可以省略大括号
- 如果元素有默认值,使用时可以不指定该元素的值
四、注解解析方式
注解解析是指读取注解中的信息并进行相应处理的过程。根据注解的生命周期,注解解析分为两种方式:运行时解析和编译时解析。
4.1 运行时解析(反射解析)
运行时解析是指在程序运行期间,通过反射获取注解信息并进行处理。这是最常用的注解解析方式,适用于RetentionPolicy.RUNTIME生命周期的注解。
4.1.1 运行时解析API
// 判断类上是否存在指定注解
boolean hasAnnotation=clazz.isAnnotationPresent(MyAnnotation.class);
// 获取类上的指定注解
MyAnnotation classAnnotation=clazz.getAnnotation(MyAnnotation.class);
// 获取方法上的指定注解
Method method=clazz.getMethod("sayHello");
MyAnnotation methodAnnotation=method.getAnnotation(MyAnnotation.class);
// 获取字段上的指定注解
Field field=clazz.getField("name");
MyAnnotation fieldAnnotation=field.getAnnotation(MyAnnotation.class);
// 获取注解元素的值
String value=classAnnotation.value();
int age=classAnnotation.age();
4.1.2 运行时解析示例
public class AnnotationParser {
public static void parseClass(Class<?> clazz) {
// 解析类上的注解
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation=clazz.getAnnotation(MyAnnotation.class);
System.out.println("类注解信息:");
System.out.println("value: " + annotation.value());
System.out.println("age: " + annotation.age());
System.out.println("gender: " + annotation.gender());
System.out.println("hobbies: " + String.join(", ", annotation.hobbies()));
}
// 解析方法上的注解
Method[] methods=clazz.getMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation=method.getAnnotation(MyAnnotation.class);
System.out.println("\n方法" + method.getName() + "注解信息:");
System.out.println("value: " + annotation.value());
}
}
}
public static void main(String[] args) {
parseClass(Person.class);
}
}
4.2 编译时解析(注解处理器)
编译时解析是指在Java代码编译期间,通过注解处理器(Annotation Processor)读取注解信息并进行处理。适用于RetentionPolicy.SOURCE和RetentionPolicy.CLASS生命周期的注解。
4.2.1 注解处理器的工作原理
- 注解处理器是
javax.annotation.processing包中的API - 它在Java编译阶段运行,在生成字节码之前处理注解
- 可以生成新的Java源文件或修改现有代码
- 常见的注解处理器:Lombok、MapStruct、AutoService
4.2.2 自定义注解处理器步骤
- 定义一个继承自
AbstractProcessor的类 - 重写
process()方法,实现注解处理逻辑 - 使用
@SupportedAnnotationTypes指定要处理的注解 - 使用
@SupportedSourceVersion指定支持的Java版本 - 注册注解处理器(通过
META-INF/services文件或@AutoService注解)
4.2.3 自定义注解处理器示例
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@SupportedAnnotationTypes("com.example.MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 遍历所有被MyAnnotation注解的元素
for (TypeElement annotation : annotations) {
roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
// 处理注解信息
MyAnnotation myAnnotation=element.getAnnotation(MyAnnotation.class);
System.out.println("编译时处理注解:" + element.getSimpleName() + ", value=" + myAnnotation.value());
// 可以在这里生成新的Java源文件
// ...
});
}
return true;
}
}
4.3 两种解析方式对比
| 对比项 | 运行时解析 | 编译时解析 |
|---|---|---|
| 执行时机 | 程序运行期间 | Java编译期间 |
| 依赖技术 | 反射 | 注解处理器 |
| 适用生命周期 | RUNTIME | SOURCE、CLASS |
| 性能 | 较差(反射开销) | 较好(编译时处理,运行时无开销) |
| 功能 | 可以动态处理,灵活性高 | 只能生成或修改代码,不能动态处理 |
| 典型应用 | Spring依赖注入、MyBatis映射 | Lombok、MapStruct、AutoService |
五、反射与注解的结合使用
反射和注解是Java中最强大的组合之一,几乎所有的Java框架都基于这两个技术实现。它们的结合可以实现:
- 配置与代码分离
- 声明式编程
- 自动化处理
5.1 典型应用:简单的依赖注入框架
// 自定义依赖注入注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
// 服务类
public class UserService {
public void addUser() {
System.out.println("添加用户");
}
}
// 控制器类
public class UserController {
@Autowired
private UserService userService;
public void addUser() {
userService.addUser();
}
}
// 简单的IoC容器
public class SimpleIocContainer {
public static <T> T getInstance(Class<T> clazz) throws Exception {
// 创建对象实例
T instance=clazz.newInstance();
// 遍历所有字段
Field[] fields=clazz.getDeclaredFields();
for (Field field : fields) {
// 如果字段上有@Autowired注解
if (field.isAnnotationPresent(Autowired.class)) {
// 设置可访问性
field.setAccessible(true);
// 获取字段类型
Class<?> fieldType=field.getType();
// 创建字段类型的实例
Object fieldInstance=getInstance(fieldType);
// 注入字段值
field.set(instance, fieldInstance);
}
}
return instance;
}
public static void main(String[] args) throws Exception {
UserController controller=getInstance(UserController.class);
controller.addUser(); // 输出:添加用户
}
}
六、常见面试题
什么是反射?反射的原理是什么?
反射是Java提供的一种动态获取类信息和动态调用对象方法的机制。原理是JVM加载类时会生成一个Class对象,这个对象包含了类的完整结构信息,通过这个对象可以在运行时操作类和对象。获取Class对象的三种方式有什么区别?
- 类名.class:编译期已知类名,最安全、性能最好
- 对象.getClass():已有对象实例时使用
- Class.forName():运行期动态加载类,如JDBC驱动加载
反射有什么优缺点?
优点:动态性、通用性、可以突破访问限制
缺点:性能开销大、安全风险、代码可读性差、编译期无法检查什么是注解?注解的本质是什么?
注解是Java 5引入的一种元数据,用于为代码添加额外信息。本质是一个继承自Annotation接口的接口。元注解有哪些?分别有什么作用?
- @Target:指定注解可以应用的目标元素类型
- @Retention:指定注解的生命周期
- @Documented:指定注解会被javadoc工具提取到API文档中
- @Inherited:指定注解具有继承性
注解的生命周期有哪几种?分别适用于什么场景?
- SOURCE:只保留在源码中,适用于编译期检查
- CLASS:保留在字节码文件中,适用于字节码级别的处理
- RUNTIME:保留在内存中,适用于运行时动态处理
注解解析方式有哪几种?分别有什么区别?
- 运行时解析:通过反射在程序运行期间处理注解,灵活性高但性能较差
- 编译时解析:通过注解处理器在Java编译期间处理注解,性能好但只能生成或修改代码
@Inherited注解的作用是什么?有什么限制?
@Inherited指定被修饰的注解具有继承性,子类会自动继承父类上的该注解。但它只对类级别的注解有效,对方法和字段级别的注解无效。
Java反射与注解 面试版核心考点清单(可直接背诵)
一、反射核心考点(面试必问)
- 反射定义:运行时动态获取类信息、动态调用对象方法的机制,核心是将类的组成部分(字段、方法、构造器)封装为对象。
- 底层原理:JVM加载
.class字节码时生成唯一的Class对象,包含类的完整结构信息,是反射的唯一入口。 - 获取Class对象的3种方式(必考):
类名.class:编译期确定,性能最好,最安全对象.getClass():已有实例时使用Class.forName("全类名"):运行期动态加载,如JDBC驱动
- 核心API分类(高频手写):
- 构造器:
getConstructors()/getDeclaredConstructors() - 字段:
getFields()/getDeclaredFields() - 方法:
getMethods()/getDeclaredMethods() - 访问控制:
setAccessible(true)突破private限制
- 构造器:
- 反射优缺点(必考):
- 优点:动态性、通用性、可编写通用框架代码
- 缺点:性能差(比直接调用慢10-100倍)、破坏封装、编译期无法检查错误
- 典型应用场景:Spring IoC、MyBatis ORM、JSON序列化、JDK动态代理、JUnit单元测试
二、注解核心考点(面试必问)
- 注解定义:Java 5引入的元数据,为代码添加额外信息,本身不影响代码执行,需通过工具解析。
- 本质:所有注解都继承自
java.lang.annotation.Annotation接口。 - 4个元注解(必考):
@Target:指定注解可应用的元素类型(TYPE、FIELD、METHOD等)@Retention:指定注解生命周期(SOURCE/CLASS/RUNTIME,默认CLASS)@Documented:注解会被javadoc提取到API文档@Inherited:类级注解可被子类继承(方法/字段级无效)
- 注解生命周期(必考):
- SOURCE:仅源码保留,编译时丢弃(如
@Override) - CLASS:字节码保留,JVM加载时丢弃(如字节码增强)
- RUNTIME:运行时保留,可通过反射获取(如Spring注解)
- SOURCE:仅源码保留,编译时丢弃(如
- 注解元素类型限制:只能是基本类型、String、Class、枚举、注解及以上类型的数组。
- 特殊语法:只有一个名为
value的元素时,使用时可省略元素名。
三、注解解析核心考点
- 两种解析方式对比(必考):
| 对比项 | 运行时解析 | 编译时解析 |
|---|---|---|
| 执行时机 | 程序运行时 | Java编译时 |
| 依赖技术 | 反射 | 注解处理器 |
| 适用生命周期 | RUNTIME | SOURCE、CLASS |
| 性能 | 较差 | 较好 |
| 典型应用 | Spring依赖注入 | Lombok、MapStruct |
- 运行时解析API:
isAnnotationPresent()、getAnnotation()、getAnnotations() - 编译时解析流程:继承
AbstractProcessor→重写process()方法→指定处理的注解→注册处理器
四、反射与注解结合核心考点
- 核心价值:实现声明式编程、配置与代码分离、自动化处理
- 典型实现:依赖注入、ORM映射、AOP切面、接口权限校验
- 关键注意点:注解必须声明为
RUNTIME生命周期才能被反射解析
10道高频手写题(含标准答案)
题1:写出获取Class对象的三种方式
public class ClassGetWays {
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:类名.class(最常用,性能最好)
Class<?> clazz1=String.class;
// 方式2:对象.getClass()
String str="hello";
Class<?> clazz2=str.getClass();
// 方式3:Class.forName()(动态加载)
Class<?> clazz3=Class.forName("java.lang.String");
// 验证:同一个类的Class对象唯一
System.out.println(clazz1==clazz2); // true
System.out.println(clazz1==clazz3); // true
}
}
考点:三种方式的区别与适用场景
题2:通过反射创建对象(含私有构造器)
class Person {
private String name;
// 私有构造器
private Person(String name) {
this.name=name;
}
@Override
public String toString() {
return "Person{name='" + name + "'}";
}
}
public class ReflectCreateObject {
public static void main(String[] args) throws Exception {
Class<?> clazz=Person.class;
// 获取私有构造器
var constructor=clazz.getDeclaredConstructor(String.class);
// 突破访问限制
constructor.setAccessible(true);
// 创建对象
Person person=(Person) constructor.newInstance("张三");
System.out.println(person); // Person{name='张三'}
}
}
考点:getDeclaredConstructor()和setAccessible(true)的使用
题3:通过反射调用私有方法
class Calculator {
private int add(int a, int b) {
return a+b;
}
}
public class ReflectInvokePrivateMethod {
public static void main(String[] args) throws Exception {
Calculator calculator=new Calculator();
Class<?> clazz=calculator.getClass();
// 获取私有方法
Method addMethod=clazz.getDeclaredMethod("add", int.class, int.class);
// 突破访问限制
addMethod.setAccessible(true);
// 调用方法
int result=(int) addMethod.invoke(calculator, 10, 20);
System.out.println(result); // 30
}
}
考点:getDeclaredMethod()和invoke()方法的使用
题4:通过反射修改私有字段
class User {
private String username="admin";
}
public class ReflectModifyField {
public static void main(String[] args) throws Exception {
User user=new User();
Class<?> clazz=user.getClass();
// 获取私有字段
Field usernameField=clazz.getDeclaredField("username");
// 突破访问限制
usernameField.setAccessible(true);
// 修改字段值
usernameField.set(user, "root");
// 验证
System.out.println(usernameField.get(user)); // root
}
}
考点:getDeclaredField()和set()/get()方法的使用
题5:自定义运行时注解并通过反射解析
// 自定义注解
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface RequestMapping {
String value() default "";
String method() default "GET";
}
// 使用注解
@RequestMapping("/user")
class UserController {
@RequestMapping(value="/add", method="POST")
public void addUser() {
}
}
// 解析注解
public class RuntimeAnnotationParser {
public static void main(String[] args) {
Class<?> clazz=UserController.class;
// 解析类上的注解
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping classAnnotation=clazz.getAnnotation(RequestMapping.class);
System.out.println("类路径:" + classAnnotation.value());
}
// 解析方法上的注解
Method[] methods=clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping methodAnnotation=method.getAnnotation(RequestMapping.class);
System.out.println("方法路径:" + methodAnnotation.value());
System.out.println("请求方法:" + methodAnnotation.method());
}
}
}
}
考点:自定义注解的语法和运行时解析流程
题6:实现简单的依赖注入(@Autowired)
// 依赖注入注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Autowired {
}
// 服务类
class UserService {
public void addUser() {
System.out.println("添加用户成功");
}
}
// 控制器类
class UserController {
@Autowired
private UserService userService;
public void addUser() {
userService.addUser();
}
}
// 简单IoC容器
public class SimpleIoc {
public static <T> T getInstance(Class<T> clazz) throws Exception {
T instance=clazz.getDeclaredConstructor().newInstance();
// 遍历所有字段
Field[] fields=clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
// 递归创建依赖对象并注入
Object dependency=getInstance(field.getType());
field.set(instance, dependency);
}
}
return instance;
}
public static void main(String[] args) throws Exception {
UserController controller=getInstance(UserController.class);
controller.addUser(); // 输出:添加用户成功
}
}
考点:反射与注解结合的核心应用,Spring IoC的基本原理
题7:实现简单的ORM映射(@Table/@Column)
// 表注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table {
String value();
}
// 列注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Column {
String value();
}
// 实体类
@Table("t_user")
class User {
@Column("id")
private Long id;
@Column("username")
private String username;
@Column("password")
private String password;
// 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 getPassword() {
return password; }
public void setPassword(String password) {
this.password=password; }
}
// ORM工具类
public class SimpleORM {
// 生成查询所有用户的SQL
public static String generateSelectAll(Class<?> clazz) {
if (!clazz.isAnnotationPresent(Table.class)) {
throw new IllegalArgumentException("类没有@Table注解");
}
Table table=clazz.getAnnotation(Table.class);
String tableName=table.value();
StringBuilder columns=new StringBuilder();
Field[] fields=clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Column.class)) {
Column column=field.getAnnotation(Column.class);
columns.append(column.value()).append(", ");
}
}
// 移除最后一个逗号
if (columns.length()>0) {
columns.delete(columns.length()-2, columns.length());
}
return "SELECT " + columns + " FROM " + tableName;
}
public static void main(String[] args) {
String sql=generateSelectAll(User.class);
System.out.println(sql); // SELECT id, username, password FROM t_user
}
}
考点:MyBatis/Hibernate等ORM框架的核心原理
题8:验证@Inherited注解的继承性
// 带继承性的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface InheritedAnnotation {
String value();
}
// 不带继承性的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface NoInheritedAnnotation {
String value();
}
// 父类
@InheritedAnnotation("父类注解")
@NoInheritedAnnotation("父类注解")
class Parent {
}
// 子类
class Child extends Parent {
}
public class InheritedTest {
public static void main(String[] args) {
Class<?> childClass=Child.class;
// 检查@InheritedAnnotation是否被继承
System.out.println(childClass.isAnnotationPresent(InheritedAnnotation.class)); // true
InheritedAnnotation inheritedAnnotation=childClass.getAnnotation(InheritedAnnotation.class);
System.out.println(inheritedAnnotation.value()); // 父类注解
// 检查@NoInheritedAnnotation是否被继承
System.out.println(childClass.isAnnotationPresent(NoInheritedAnnotation.class)); // false
}
}
考点:@Inherited注解的作用和限制(仅对类级注解有效)
题9:反射获取方法的泛型返回类型
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
class UserService {
public List<String> getUsernames() {
return null;
}
}
public class GenericTypeTest {
public static void main(String[] args) throws Exception {
Method method=UserService.class.getMethod("getUsernames");
// 获取泛型返回类型
Type returnType=method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
ParameterizedType parameterizedType=(ParameterizedType) returnType;
// 获取原始类型(List)
Type rawType=parameterizedType.getRawType();
System.out.println("原始类型:" + rawType); // interface java.util.List
// 获取泛型参数类型(String)
Type[] actualTypeArguments=parameterizedType.getActualTypeArguments();
for (Type type : actualTypeArguments) {
System.out.println("泛型参数:" + type); // class java.lang.String
}
}
}
}
考点:反射处理泛型的方法,解决泛型擦除问题
题10:自定义编译时注解处理器(生成代码)
// 自定义编译时注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
@interface GenerateBuilder {
}
// 注解处理器(需要单独放在一个模块中)
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.Writer;
import java.util.Set;
@SupportedAnnotationTypes("com.example.GenerateBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BuilderAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
roundEnv.getElementsAnnotatedWith(annotation).forEach(element -> {
if (element instanceof TypeElement) {
TypeElement typeElement=(TypeElement) element;
generateBuilderClass(typeElement);
}
});
}
return true;
}
private void generateBuilderClass(TypeElement typeElement) {
String className=typeElement.getSimpleName().toString();
String builderClassName=className + "Builder";
String packageName=processingEnv.getElementUtils().getPackageOf(typeElement).toString();
try {
JavaFileObject builderFile=processingEnv.getFiler().createSourceFile(packageName + "." + builderClassName);
Writer writer=builderFile.openWriter();
writer.write("package " + packageName + ";\n\n");
writer.write("public class " + builderClassName + " {\n");
writer.write(" private " + className + " instance=new " + className + "();\n\n");
writer.write(" public " + builderClassName + " build() {\n");
writer.write(" return instance;\n");
writer.write(" }\n");
writer.write("}\n");
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
考点:编译时注解处理器的基本结构和代码生成方法,Lombok的核心原理
Java反射与注解 一页纸速记版(面试前5分钟必看)
🔹 反射核心速记
- 本质:运行时操作Class对象,将类成员封装为Field/Method/Constructor
- 3种Class获取:类名.class(最优) > 对象.getClass() > Class.forName(动态)
- API口诀:带Declared拿所有,不带拿public;setAccessible(true)破私有
- 性能问题:比直接调用慢10-100倍,原因:类解析、安全检查、方法调用开销
- 应用:Spring IoC、MyBatis、JSON序列化、动态代理
🔹 注解核心速记
- 本质:继承自Annotation接口的特殊接口
- 4个元注解:Target(作用范围)、Retention(生命周期)、Documented(文档)、Inherited(继承)
- 3种生命周期:SOURCE(编译) → CLASS(字节码) → RUNTIME(运行),默认CLASS
- 元素类型:基本类型、String、Class、枚举、注解及它们的数组
- 特殊语法:单value元素可省略名称
🔹 注解解析速记
- 运行时解析:反射+RUNTIME注解,灵活但慢,Spring全家桶
- 编译时解析:注解处理器+SOURCE/CLASS注解,性能好,Lombok/MapStruct
- 核心API:isAnnotationPresent()、getAnnotation()、getAnnotations()
🔹 结合应用速记
- 核心价值:声明式编程、配置与代码分离
- 典型实现:@Autowired依赖注入、@Table ORM映射、@RequestMapping路由
- 关键前提:运行时解析必须使用RUNTIME生命周期注解
5道进阶面试题(含深度解析)
题1:反射为什么比直接调用慢?如何优化反射性能?
答案:
反射慢的核心原因:
- 类结构解析开销:每次反射调用都需要动态解析类的字节码结构
- 安全检查开销:JVM会执行访问权限检查,即使setAccessible(true)也有开销
- 方法调用开销:反射调用是通过Method.invoke()间接调用,无法被JIT编译器优化
- 参数装箱拆箱:invoke()方法参数是Object数组,基本类型需要装箱拆箱
- 返回值类型转换:返回值是Object,需要强制类型转换
优化方法:
- 缓存Class对象:避免重复调用Class.forName()
- 缓存反射对象:将Field、Method、Constructor对象缓存起来复用
- 使用setAccessible(true):关闭访问权限检查,提升30%-50%性能
- 使用反射工具类:如Apache Commons Lang的MethodUtils、FieldUtils
- 使用字节码生成技术:如ASM、CGLIB,生成直接调用的字节码,性能接近直接调用
- Java 9+优化:使用MethodHandles.Lookup,性能比传统反射提升2-3倍
考点:对反射底层原理的深入理解,以及实际开发中的性能优化经验
题2:为什么注解的元素类型只能是基本类型、String、Class、枚举、注解及它们的数组?
答案:
这是由Java注解的设计和实现原理决定的:
- 编译期常量要求:注解的元素值必须在编译期就能确定,是编译期常量
- 字节码存储限制:注解信息存储在字节码的RuntimeVisibleAnnotations属性中,只能存储常量池中的常量
- 类型安全考虑:限制类型可以保证注解的类型安全,避免运行时类型转换错误
- 解析效率考虑:简单类型的解析效率更高,复杂对象无法在编译期序列化到字节码中
反例:如果允许使用自定义对象作为注解元素,那么在编译期无法确定对象的值,也无法将其序列化到字节码中,运行时解析时会出现问题。
考点:对注解底层实现原理的理解
题3:如何通过反射获取方法参数的注解?为什么JDK 8之前无法获取方法参数名?
答案:
获取方法参数注解的代码:
Method method=clazz.getMethod("test", String.class, int.class);
Annotation[][] parameterAnnotations=method.getParameterAnnotations();
for (int i=0; i<parameterAnnotations.length; i++) {
System.out.println("参数" + (i+1) + "的注解:");
for (Annotation annotation : parameterAnnotations[i]) {
System.out.println(annotation);
}
}
JDK 8之前无法获取方法参数名的原因:
- 字节码中不存储参数名:Java编译时默认不会将方法参数名写入字节码文件,只存储参数类型和顺序
- 节省字节码空间:早期Java为了减小字节码文件大小,省略了参数名信息
- JDK 8的改进:JDK 8引入了
-parameters编译参数,开启后会将方法参数名写入字节码,此时可以通过Parameter.getName()获取参数名
代码示例(JDK 8+):
Method method=clazz.getMethod("test", String.class, int.class);
Parameter[] parameters=method.getParameters();
for (Parameter parameter : parameters) {
System.out.println("参数名:" + parameter.getName());
}
考点:反射处理方法参数的细节,以及JDK版本差异带来的影响
题4:Spring中@Transactional注解的实现原理(反射+AOP)
答案:
@Transactional注解的实现基于反射和AOP(面向切面编程),核心流程:
- 注解扫描:Spring容器启动时,通过反射扫描所有被@Transactional注解标记的类和方法
- 动态代理生成:为目标类生成动态代理对象(JDK动态代理或CGLIB动态代理)
- 事务拦截器:代理对象在调用目标方法时,会被TransactionInterceptor拦截
- 事务管理:
- 方法执行前:通过反射获取方法上的@Transactional注解属性
- 根据注解属性创建事务(传播行为、隔离级别、超时时间等)
- 执行目标方法
- 方法正常返回:提交事务
- 方法抛出异常:根据注解的rollbackFor属性判断是否回滚事务
关键细节:
- @Transactional注解必须标注在public方法上,因为Spring AOP只能拦截public方法
- 同一个类内部调用@Transactional方法不会生效,因为不会经过代理对象
- 注解的属性会覆盖类上的注解属性
考点:Spring核心原理,反射与AOP的结合应用
题5:泛型擦除后,反射如何获取泛型信息?
答案:
Java泛型采用擦除式实现,泛型信息在编译期会被擦除,但以下情况泛型信息会被保留在字节码中:
- 类的泛型父类
- 类的泛型接口
- 字段的泛型类型
- 方法的泛型返回类型
- 方法的泛型参数类型
反射通过java.lang.reflect.Type接口及其子接口获取这些保留的泛型信息:
ParameterizedType:表示参数化类型,如List<String>GenericArrayType:表示泛型数组类型,如List<String>[]TypeVariable:表示类型变量,如TWildcardType:表示通配符类型,如? extends Number
代码示例(获取类的泛型父类):
class Parent<T> {
}
class Child extends Parent<String> {
}
public class GenericTest {
public static void main(String[] args) {
Type superType=Child.class.getGenericSuperclass();
if (superType instanceof ParameterizedType) {
ParameterizedType parameterizedType=(ParameterizedType) superType;
Type[] actualTypeArguments=parameterizedType.getActualTypeArguments();
System.out.println("泛型参数类型:" + actualTypeArguments[0]); // class java.lang.String
}
}
}
考点:泛型擦除的原理,以及反射如何绕过泛型擦除获取泛型信息