泛型、反射和注解是Java框架开发中的重要基础。
泛型主要解决类型安全和代码复用问题;反射允许程序在运行期间获取和操作类的信息;注解用于给代码添加结构化元数据。Spring、MyBatis等框架正是将反射和注解结合起来,完成对象创建、依赖注入、方法拦截和配置解析。

一、什么是Java泛型
泛型允许在定义类、接口或方法时,把具体类型作为参数传入。
例如,不使用泛型时,可以把任何对象放入集合:
List list = new ArrayList();
list.add("Java");
list.add(100);
取出数据时,程序需要手动进行类型转换:
String value = (String) list.get(0);
如果转换类型不正确,就会在运行时抛出ClassCastException。
使用泛型后,可以明确集合中允许保存的数据类型:
List<String> list = new ArrayList<>();
list.add("Java");
此时编译器能够在编译阶段进行类型检查,避免把Integer等其他类型放入集合。
泛型的主要作用包括:
-
提高类型安全性;
-
减少强制类型转换;
-
提高代码复用能力;
-
将部分运行时错误提前到编译阶段发现。
二、Java泛型为什么采用类型擦除
Java泛型主要通过类型擦除实现。
所谓类型擦除,是指泛型类型信息主要用于编译阶段的类型检查。编译完成后,大部分具体泛型参数不会直接保留在运行时对象中。
例如:
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
在运行期间,它们的实际类型都是ArrayList:
System.out.println(
stringList.getClass() == integerList.getClass()
);
结果为:
true
JVM通常不会把它们分别看成ArrayList<String>和ArrayList<Integer>两个不同的运行时类。
1. 为什么使用类型擦除
Java在早期版本中没有泛型。泛型是在JDK 5中加入的。
为了让引入泛型后的代码仍然能够与旧版本Java代码和旧类库兼容,Java没有要求JVM重新设计一套完全不同的泛型类型系统,而是主要在编译器层面实现泛型。
编译器负责完成以下工作:
-
检查泛型类型是否正确;
-
对泛型代码进行必要的类型转换;
-
将泛型参数擦除为上界类型或
Object; -
在需要的位置自动插入强制类型转换。
例如:
public class Box<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
如果T没有明确上界,类型擦除后可以近似理解为:
public class Box {
private Object value;
public void setValue(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
调用者使用Box<String>时,编译器会在取出数据的位置自动插入类型转换。
如果泛型具有上界:
public class NumberBox<T extends Number> {
private T value;
}
类型擦除后,T会被擦除为它的上界Number,而不是Object。
2. 类型擦除带来了什么好处
类型擦除使泛型代码能够兼容旧版本代码,也避免了JVM为每一种泛型参数分别创建新的类。
例如,程序不需要分别生成:
ArrayList<String>
ArrayList<Integer>
ArrayList<User>
对应的三个运行时类,而是复用同一个ArrayList类。
这样可以减少运行时类型数量,也保持了Java二进制兼容性。
三、类型擦除有哪些限制
类型擦除提高了兼容性,但也带来了一些限制。
1. 运行时不能直接获取具体泛型参数
例如:
List<String> list = new ArrayList<>();
运行时通过list.getClass()通常只能得到ArrayList,不能直接得到当前对象的泛型参数一定是String。
因此,下面的判断是不允许的:
if (obj instanceof List<String>) {
}
可以判断:
if (obj instanceof List<?>) {
}
因为运行时无法可靠地区分List<String>和List<Integer>。
需要注意的是,类、字段、方法声明中的部分泛型签名可能保存在Class文件的元数据中,并可以通过反射读取。但普通对象本身通常不会保存"这个实例是List<String>"这样的完整信息。
2. 不能直接创建泛型类型对象
下面的代码不能通过编译:
T value = new T();
因为类型擦除后,JVM不知道应该调用哪个具体类的构造方法。
通常需要传入Class<T>或对象创建函数:
public static <T> T create(Class<T> type)
throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance();
}
3. 不能直接创建泛型数组
下面的代码同样不合法:
T[] array = new T[10];
Java数组在运行时需要知道具体元素类型,而泛型类型经过擦除后无法提供这一信息。
实际开发中可以使用集合,或者通过反射创建数组:
@SuppressWarnings("unchecked")
T[] array = (T[]) Array.newInstance(type, 10);
4. 静态成员不能使用类的泛型参数
下面的写法不能通过编译:
class Box<T> {
private static T value;
}
静态字段属于类,而不是某个具体对象。
同一个Box类可以创建Box<String>和Box<Integer>对象。如果静态字段使用T,JVM无法确定它究竟应该是String还是Integer。
5. 基本数据类型不能直接作为泛型参数
下面的写法是不允许的:
List<int> numbers;
泛型参数必须是引用类型,因此需要使用包装类:
List<Integer> numbers;
四、<? extends T>和<? super T>有什么区别
通配符通常用于增强泛型方法的适用范围。
最常见的两种形式是:
<? extends T>
<? super T>
可以使用PECS原则记忆:
Producer Extends,Consumer Super
也就是:
-
主要从容器中读取数据时,使用
extends; -
主要向容器中写入数据时,使用
super。
五、<? extends T>:适合读取
假设有如下类型关系:
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
方法接收:
List<? extends Animal> animals
表示这个集合的实际类型可能是:
List<Animal>
List<Dog>
List<Cat>
从这个集合中读取数据是安全的:
Animal animal = animals.get(0);
因为无论集合实际保存的是Animal、Dog还是Cat,都可以向上转型为Animal。
但是,一般不能向其中添加具体对象:
animals.add(new Dog()); // 编译错误
animals.add(new Cat()); // 编译错误
原因是编译器不知道这个集合真实类型究竟是List<Dog>还是List<Cat>。
如果真实类型是List<Cat>,向其中加入Dog就会破坏类型安全。
因此,extends的特点是:
-
可以安全读取为上界类型;
-
不能随意写入具体子类对象;
-
适合作为数据生产者。
六、<? super T>:适合写入
例如:
List<? super Dog> dogs
表示集合的实际类型可能是:
List<Dog>
List<Animal>
List<Object>
此时可以安全地加入Dog或Dog的子类对象:
dogs.add(new Dog());
因为无论真实集合是List<Dog>、List<Animal>还是List<Object>,都能够保存Dog对象。
但是,从集合中读取时,只能保证结果是Object:
Object value = dogs.get(0);
不能直接写成:
Dog dog = dogs.get(0);
因为集合实际可能是List<Animal>,里面可能还保存了其他类型的动物。
因此,super的特点是:
-
可以安全写入
T及其子类对象; -
读取时通常只能按
Object接收; -
适合作为数据消费者。
典型的复制方法可以设计为:
public static <T> void copy(
List<? extends T> source,
List<? super T> target) {
for (T value : source) {
target.add(value);
}
}
source负责生产数据,所以使用extends;target负责接收数据,所以使用super。

七、什么是反射
反射是指程序在运行期间动态获取类的信息,并操作类或对象的能力。
普通调用方式通常在编译阶段就确定了类型和方法:
UserService service = new UserService();
service.createUser();
反射可以通过类名、配置或注解,在运行时决定创建哪个对象、调用哪个方法:
Class<?> type = Class.forName(
"com.example.UserService"
);
Object object = type.getDeclaredConstructor()
.newInstance();
Method method = type.getDeclaredMethod("createUser");
method.invoke(object);
通过反射可以获取:
-
类名、父类和接口;
-
构造方法;
-
字段;
-
普通方法;
-
注解;
-
泛型声明信息;
-
方法参数和返回值信息。
八、反射的实现原理是什么
Java类被JVM加载后,会在运行时保存对应的类型元数据,并提供一个Class对象作为访问入口。
获得Class对象的常见方式包括:
Class<User> type1 = User.class;
Class<?> type2 = user.getClass();
Class<?> type3 = Class.forName(
"com.example.User"
);
反射API通过Class对象查询类的结构,并使用Field、Method、Constructor等对象描述字段、方法和构造器。
例如:
Method method = type.getDeclaredMethod(
"save",
User.class
);
method.invoke(service, user);
Method.invoke()会完成参数检查、访问权限检查、调用目标定位、参数包装和异常封装等工作,最终执行对应方法。
现代JVM也会对频繁执行的反射调用进行一定优化,因此不能简单认为所有反射调用始终极慢。但在相同条件下,直接调用通常仍然更容易被JVM分析和优化。
九、为什么反射通常比直接调用慢
直接调用时,编译器和JVM能够较早确定方法签名和调用目标,并进行内联等优化。
反射调用则通常需要额外完成:
-
查找类、方法或字段;
-
检查访问权限;
-
检查参数数量和类型;
-
对基本类型进行装箱或拆箱;
-
通过通用调用入口执行方法;
-
将目标方法抛出的异常包装为反射异常;
-
降低部分JIT优化的确定性。
不过,反射性能问题需要结合使用场景判断。
Spring通常在应用启动阶段通过反射扫描类、创建Bean和完成依赖注入。启动完成后,大部分业务方法依然是普通方法调用。因此,框架使用反射并不代表每一次业务操作都进行大量反射扫描。
实际开发中还可以缓存Class、Method、Field等对象,避免重复查找。

十、什么是Java注解
注解是一种可以附加在类、方法、字段、参数等程序元素上的结构化元数据。
例如:
@Override
public String toString() {
return "User";
}
@Override告诉编译器,当前方法应该重写父类或接口中的方法。
自定义注解可以这样定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {
String value();
boolean recordParams() default true;
}
从语法和字节码层面看,注解是一种特殊的接口声明形式。定义注解时使用@interface,编译器会将注解信息写入Class文件的相应元数据结构中。
十一、注解中的元注解有什么作用
元注解是用于修饰其他注解的注解。
1. @Target
规定注解可以出现在哪些位置:
@Target(ElementType.METHOD)
表示该注解只能用于方法。
常见位置包括:
-
TYPE:类、接口或枚举; -
METHOD:方法; -
FIELD:字段; -
PARAMETER:参数; -
CONSTRUCTOR:构造方法。
2. @Retention
规定注解保留到哪个阶段。
@Retention(RetentionPolicy.RUNTIME)
常见策略包括:
-
SOURCE:只存在于源代码中; -
CLASS:写入Class文件,但运行时不一定可通过反射获取; -
RUNTIME:运行时仍然保留,可以通过反射读取。
框架需要在运行期间读取的注解,一般使用RUNTIME。
3. @Inherited
表示类上的注解可以被子类继承。
需要注意的是,它主要作用于类级别注解,不代表接口注解或方法注解都会自动继承。
4. @Documented
表示生成Java文档时,可以把该注解包含在文档中。
十二、运行时注解是如何被框架读取的
假设定义一个运行时注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
业务类使用该注解:
@Component("userService")
public class UserService {
}
框架启动时可以扫描指定包下的类,获得对应的Class对象,然后判断类上是否存在该注解:
if (type.isAnnotationPresent(Component.class)) {
Component component =
type.getAnnotation(Component.class);
String beanName = component.value();
}
读取方法注解也是类似的:
for (Method method : type.getDeclaredMethods()) {
if (method.isAnnotationPresent(
OperationLog.class)) {
OperationLog log =
method.getAnnotation(
OperationLog.class
);
}
}
框架可以根据注解中的信息决定:
-
是否创建对象;
-
Bean使用什么名称;
-
字段是否需要注入;
-
方法是否需要事务;
-
接口映射到哪个请求地址;
-
参数如何绑定;
-
方法是否需要日志、权限或缓存拦截。
注解本身通常只负责描述信息,真正执行功能的是读取和处理注解的框架代码。
十三、Spring为什么大量使用反射和注解

Spring的核心目标之一是降低业务代码和对象创建逻辑之间的耦合。
如果完全不使用反射,创建对象通常需要直接编写:
UserRepository repository =
new UserRepository();
UserService service =
new UserService(repository);
Spring通过注解描述组件关系:
@Service
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}
容器启动时通过扫描和反射完成:
-
找到带有
@Component、@Service等注解的类; -
分析构造方法和依赖关系;
-
通过反射创建对象;
-
将所需依赖注入对象;
-
将对象注册到IOC容器;
-
根据事务、缓存等注解决定是否生成代理对象。
Spring MVC也会读取:
@RestController
@RequestMapping
@GetMapping
@RequestBody
等注解,建立请求地址、控制器方法和参数之间的映射。
因此,注解负责声明"这个类或方法需要什么能力",反射负责在运行时读取这些声明并完成实际操作。
Spring大量使用反射和注解,主要获得了以下好处:
-
减少重复配置代码;
-
降低对象之间的直接耦合;
-
支持依赖注入;
-
支持面向切面编程;
-
支持统一事务和权限控制;
-
让框架功能能够以声明式方式使用。
不过,注解和反射并不是越多越好。业务逻辑过度依赖隐藏的框架行为,也可能降低代码可读性和问题排查效率。合理的做法是让注解负责稳定的横切能力,而不是把复杂业务流程全部隐藏在注解中。

