全栈学习 ——javaSE 泛型、反射与注解
自定义注解时,需要用 “元注解”(注解的注解)指定注解的“作用范围”“保留策略” 等。Java 提供 4个元注解(定义在元注解名称作用常用值@Target指定注解可使用的位置(如方法、类)(方法)、(类)等@Retention指定注解的保留策略(生命周期)(运行时保留,可反射获取)标记注解会被 javadoc 文档记录无参数@Inherited标记注解可被子类继承无参数核心元注解@Target和@
目录
泛型、反射与注解是 JavaSE 中支撑 “灵活编程” 与 “框架设计”
的核心技术。泛型解决 “类型安全” 问题,反射实现
“运行时动态操作类”,注解提供 “代码标记与元数据” 能力 —— 三者结合构成了
Java 框架(如
Spring、MyBatis)的底层基础。本章节将系统讲解这三项技术的核心原理与实际应用。
一、泛型(Generic):编译时的类型安全保障
在泛型出现之前,集合(如List
)默认存储Object
类型,取出元素时需强制转换,容易出现ClassCastException
(类型转换异常)。泛型通过
“编译时类型指定”,让集合只能存储特定类型元素,从源头避免类型错误。
1.1 泛型的核心作用
- 编译时类型检查:限制集合(或泛型类)只能存储指定类型元素,编译阶段就报错,而非运行时崩溃。
- 避免强制转换:取出元素时无需手动转换(编译器自动确认类型)。
- 代码复用:一套逻辑支持多种类型(如
List<String>
、List<Integer>
共用List
的实现)。
1.2 泛型的基本用法
1.2.1 泛型类与泛型接口
泛型类 / 接口在定义时声明
“类型参数”(如<T>
),使用时指定具体类型(如String
)。
泛型类示例:
// 定义泛型类:声明类型参数T(Type的缩写,可自定义名称)
class GenericBox<T> {
// 使用T作为类型(类似变量,代表一种类型)
private T value;
// T作为方法参数和返回值
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
public class GenericClassDemo {
public static void main(String[] args) {
// 使用时指定类型为String(只能存String)
GenericBox<String> stringBox = new GenericBox<>();
stringBox.setValue("Hello"); // 正确:存入String
// stringBox.setValue(123); // 编译错误:不能存Integer
// 取出时无需转换(自动为String类型)
String str = stringBox.getValue();
System.out.println(str); // 输出:Hello
// 指定类型为Integer
GenericBox<Integer> intBox = new GenericBox<>();
intBox.setValue(100);
Integer num = intBox.getValue(); // 无需转换
System.out.println(num); // 输出:100
}
}
泛型接口示例:
// 定义泛型接口(支持多种类型的“生产者”)
interface Producer<T> {
T produce();
}
// 实现泛型接口时指定具体类型(如String)
class StringProducer implements Producer<String> {
@Override
public String produce() {
return "生产的字符串";
}
}
// 实现时保留泛型(让子类也成为泛型类)
class NumberProducer<T extends Number> implements Producer<T> {
private T value;
public NumberProducer(T value) {
this.value = value;
}
@Override
public T produce() {
return value;
}
}
public class GenericInterfaceDemo {
public static void main(String[] args) {
Producer<String> strProducer = new StringProducer();
String str = strProducer.produce();
System.out.println(str); // 输出:生产的字符串
Producer<Integer> intProducer = new NumberProducer<>(100);
Integer num = intProducer.produce();
System.out.println(num); // 输出:100
}
}
1.2.2 泛型方法
泛型方法在方法声明时独立声明类型参数(与类是否泛型无关),适用于
“单个方法需要支持多种类型” 的场景。
class GenericMethodDemo {
// 定义泛型方法:<E>是方法的类型参数,声明在返回值前
public <E> void printArray(E[] array) {
for (E element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// 泛型方法带返回值
public <E> E getFirstElement(E[] array) {
if (array != null && array.length > 0) {
return array[0];
}
return null;
}
}
public class TestGenericMethod {
public static void main(String[] args) {
GenericMethodDemo demo = new GenericMethodDemo();
// 调用时自动推断类型(无需显式指定)
String[] strArray = {"A", "B", "C"};
demo.printArray(strArray); // 输出:A B C
Integer[] intArray = {1, 2, 3};
demo.printArray(intArray); // 输出:1 2 3
// 获取第一个元素(自动返回对应类型)
String firstStr = demo.getFirstElement(strArray);
Integer firstInt = demo.getFirstElement(intArray);
System.out.println("字符串数组第一个元素:" + firstStr); // 输出:A
System.out.println("整数数组第一个元素:" + firstInt); // 输出:1
}
}
泛型方法特点:
- 类型参数声明在方法返回值前(如
<E>
),与类的泛型参数无关。- 调用时编译器自动推断类型(无需手动指定,如传入
String[]
则E
自动为String
)。
1.2.3 类型通配符(Wildcard)
当需要处理 “未知类型的泛型”
时(如方法参数需要接收任意泛型List
),使用通配符?
及限定符(extends
、super
)控制类型范围。
通配符形式 | 含义 | 适用场景 |
---|---|---|
<?> |
任意类型(无限制) | 仅读取元素,不修改(如打印任意 List) |
<? extends T> |
上限:只能是 T 或 T 的子类 | 读取(可获取 T 类型),不能添加(除 null) |
<? super T> |
下限:只能是 T 或 T 的父类 | 添加(可添加 T 或子类),读取只能到 Object |
通配符示例:
import java.util.ArrayList;
import java.util.List;
public class WildcardDemo {
// 1. 无限制通配符<?>:接收任意List,只能读,不能写(除null)
public static void printList(List<?> list) {
for (Object obj : list) { // 只能用Object接收
System.out.print(obj + " ");
}
System.out.println();
// list.add("A"); // 编译错误:无法确定类型,不能添加非null元素
}
// 2. 上限通配符<? extends Number>:只能接收Number或其子类(如Integer、Double)
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) { // 可安全转为Number
total += num.doubleValue(); // 调用Number的方法
}
return total;
}
// 3. 下限通配符<? super Integer>:只能接收Integer或其父类(如Number、Object)
public static void addIntegers(List<? super Integer> list) {
list.add(10); // 可添加Integer(或其子类,如Integer本身)
list.add(20);
// Integer num = list.get(0); // 编译错误:只能用Object接收
}
public static void main(String[] args) {
// 测试<?>
List<String> strList = List.of("A", "B");
List<Integer> intList = List.of(1, 2);
printList(strList); // 输出:A B
printList(intList); // 输出:1 2
// 测试<? extends Number>
List<Integer> integerList = List.of(1, 2, 3);
List<Double> doubleList = List.of(1.5, 2.5);
System.out.println("整数和:" + sum(integerList)); // 输出:6.0
System.out.println("小数和:" + sum(doubleList)); // 输出:4.0
// 测试<? super Integer>
List<Number> numberList = new ArrayList<>();
addIntegers(numberList); // 向NumberList添加Integer
System.out.println("添加后:" + numberList); // 输出:[10, 20]
}
}
1.3 泛型的局限
- 不能用基本类型:泛型参数只能是引用类型(如
List<int>
错误,需用List<Integer>
)。 - 运行时擦除:泛型信息在编译后被擦除(如
List<String>
和List<Integer>
运行时都是List
),无法通过instanceof
判断泛型类型。 - 静态方法不能用类的泛型参数:静态方法属于类,而泛型参数随对象变化,若需泛型需定义为泛型方法。
二、反射(Reflection):运行时的类信息操作
反射允许程序在运行时获取类的信息(如类名、属性、方法、构造器),并动态操作这些成分(如调用私有方法、修改私有属性)。这打破了
“编译时确定代码逻辑” 的限制,让程序更灵活(也是框架实现
“自动装配”“依赖注入” 的核心)。
2.1 反射的核心作用
- 运行时获取类信息:无需提前知道类名,就能获取类的属性、方法等元数据。
- 动态创建对象:通过类信息动态实例化对象(如
Class.newInstance()
)。- 动态调用方法:包括私有方法(通过反射可绕过访问权限)。
- 动态操作属性:包括私有属性(可修改值)。
2.2 反射的核心类
反射的所有操作都基于java.lang.Class
类(类对象),它是反射的 “入口”。
核心类 / 接口 | 作用 |
---|---|
Class |
类的元数据对象,代表一个类的信息 |
Constructor |
类的构造器对象,用于创建实例 |
Method |
类的方法对象,用于调用方法 |
Field |
类的属性对象,用于访问 / 修改属性值 |
2.3 反射的基本操作
2.3.1 获取 Class 对象(反射入口)
获取Class
对象有 3 种方式,根据场景选择:
class Student {
private String name;
private int age;
public Student() {}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void study() {
System.out.println(name + "正在学习");
}
private String getInfo() {
return "姓名:" + name + ",年龄:" + age;
}
}
public class GetClassDemo {
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:通过对象.getClass()(已知对象)
Student student = new Student();
Class<?> clazz1 = student.getClass();
System.out.println("方式1:" + clazz1.getName()); // 输出:Student
// 方式2:通过类名.class(已知类名,编译时确定)
Class<?> clazz2 = Student.class;
System.out.println("方式2:" + clazz2.getName()); // 输出:Student
// 方式3:通过Class.forName("全类名")(仅知类名,运行时动态获取,最常用)
Class<?> clazz3 = Class.forName("Student"); // 全类名:包名+类名(此处默认无包)
System.out.println("方式3:" + clazz3.getName()); // 输出:Student
// 验证:同一个类的Class对象唯一
System.out.println(clazz1 == clazz2); // 输出:true
System.out.println(clazz1 == clazz3); // 输出:true
}
}
说明:一个类的Class
对象在 JVM 中唯一,是类加载的产物(类加载时 JVM
自动创建Class
对象)。
2.3.2 反射创建对象(通过构造器)
通过Class
对象获取Constructor
,再调用newInstance()
创建实例(支持无参和有参构造)。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ReflectNewInstance {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
// 1. 获取Class对象
Class<?> clazz = Class.forName("Student");
// 2. 方式1:调用无参构造(若类无无参构造,会抛异常)
Student student1 = (Student) clazz.newInstance(); // 已过时,推荐用Constructor
System.out.println("无参构造创建:" + student1);
// 3. 方式2:调用有参构造(更灵活,推荐)
// 获取有参构造器(参数为String和int)
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 传入参数创建实例
Student student2 = (Student) constructor.newInstance("张三", 18);
System.out.println("有参构造创建:" + student2);
}
}
2.3.3 反射调用方法(包括私有方法)
通过Method
对象调用方法,支持公有和私有方法(私有方法需先设置setAccessible(true)
取消访问检查)。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectInvokeMethod {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Class<?> clazz = Class.forName("Student");
Student student = (Student) clazz.getConstructor(String.class, int.class).newInstance("张三", 18);
// 1. 调用公有方法(study())
// 获取方法:参数1为方法名,参数2为参数类型(无参则不写)
Method studyMethod = clazz.getMethod("study");
// 调用方法:参数1为实例对象,参数2为方法参数(无参则不写)
studyMethod.invoke(student); // 输出:张三正在学习
// 2. 调用私有方法(getInfo())
// 获取私有方法需用getDeclaredMethod(getMethod只能获取公有)
Method getInfoMethod = clazz.getDeclaredMethod("getInfo");
// 取消访问检查(关键:私有方法必须设置,否则抛异常)
getInfoMethod.setAccessible(true);
// 调用私有方法
String info = (String) getInfoMethod.invoke(student);
System.out.println("私有方法返回:" + info); // 输出:姓名:张三,年龄:18
}
}
2.3.4 反射操作属性(包括私有属性)
通过Field
对象访问或修改属性,私有属性同样需要setAccessible(true)
。
import java.lang.reflect.Field;
public class ReflectOperateField {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class<?> clazz = Class.forName("Student");
Student student = (Student) clazz.getConstructor().newInstance(); // 无参构造创建
// 1. 获取并修改私有属性name
Field nameField = clazz.getDeclaredField("name"); // 获取私有属性
nameField.setAccessible(true); // 取消访问检查
nameField.set(student, "李四"); // 设置属性值(参数1为实例,参数2为值)
// 2. 获取并修改私有属性age
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.set(student, 20);
// 验证修改结果(调用之前的私有方法getInfo())
Method getInfoMethod = clazz.getDeclaredMethod("getInfo");
getInfoMethod.setAccessible(true);
String info = (String) getInfoMethod.invoke(student);
System.out.println("修改后信息:" + info); // 输出:姓名:李四,年龄:20
}
}
2.4 反射的应用场景
- 框架底层:Spring 的 IOC 容器通过反射创建对象并注入依赖;MyBatis
通过反射将数据库结果集映射为 Java 对象。- 注解解析:自定义注解需配合反射获取注解标记的类 /
方法,执行对应逻辑(如权限校验)。- 动态代理:AOP 的动态代理(如 JDK 代理)基于反射实现方法增强。
- 工具类:如 JSON
序列化工具(Jackson、FastJSON)通过反射获取对象属性并转为 JSON。
2.5 反射的优缺点
- 优点:灵活性高,支持运行时动态操作,是框架的核心技术。
- 缺点:
- 性能损耗:反射操作绕开编译优化,性能比直接调用低(但框架中影响可接受)。
- 破坏封装:可直接访问私有成员,可能导致代码逻辑混乱。
- 可读性差:反射代码较繁琐,不如直接调用直观。
三、注解(Annotation):代码的标记与元数据
注解(Annotation)是 Java 5 引入的特性,本质是
“代码的标记”,可在类、方法、属性等元素上添加,用于携带
“元数据”(描述数据的数据)。注解本身不直接影响代码逻辑,但可通过反射解析注解,执行对应操作。
3.1 注解的核心作用
- 标记代码:如
@Override
标记方法重写,编译器会校验是否符合重写规则。- 携带元数据:如
@Test
标记测试方法,测试框架会自动执行标记的方法。- 简化配置:替代 XML 配置(如 Spring
的@Controller
标记控制器类)。
3.2 常用内置注解
Java 内置了 3 个基本注解(定义在java.lang
包),编译器会识别并处理:
注解名称 | 作用 | 使用位置 |
---|---|---|
@Override |
标记方法为重写父类的方法,编译器校验 | 方法 |
@Deprecated |
标记元素(类、方法等)已过时,编译器警告 | 类、方法、属性等 |
@SuppressWarnings |
抑制编译器警告(如未使用变量警告) | 类、方法等 |
内置注解示例:
public class BuiltInAnnotationDemo {
// @Deprecated:标记方法已过时
@Deprecated
public void oldMethod() {
System.out.println("这是过时的方法");
}
// @Override:标记方法重写(若父类无此方法,编译报错)
@Override
public String toString() {
return "BuiltInAnnotationDemo对象";
}
// @SuppressWarnings:抑制“未使用变量”警告
@SuppressWarnings("unused")
public void test() {
int unusedVar = 10; // 若没有@SuppressWarnings,编译器会警告“变量未使用”
// 调用过时方法(编译器会警告,但可执行)
oldMethod();
}
public static void main(String[] args) {
new BuiltInAnnotationDemo().test();
}
}
3.3 元注解:定义注解的注解
自定义注解时,需要用 “元注解”(注解的注解)指定注解的
“作用范围”“保留策略” 等。Java 提供 4
个元注解(定义在java.lang.annotation
包):
元注解名称 | 作用 | 常用值 |
---|---|---|
@Target |
指定注解可使用的位置(如方法、类) | ElementType.METHOD (方法)、ElementType.TYPE (类)等 |
@Retention |
指定注解的保留策略(生命周期) | RetentionPolicy.RUNTIME (运行时保留,可反射获取) |
@Documented |
标记注解会被 javadoc 文档记录 | 无参数 |
@Inherited |
标记注解可被子类继承 | 无参数 |
核心元注解:@Target
和@Retention
是自定义注解必须的
——@Target
限制使用位置,@Retention(RetentionPolicy.RUNTIME)
确保注解在运行时存在(才能被反射解析)。
3.4 自定义注解及解析
自定义注解需配合反射使用:先定义注解,再在代码中标记,最后通过反射解析注解并执行逻辑。
示例:自定义权限校验注解
import java.lang.annotation.*;
import java.lang.reflect.Method;
// 1. 定义自定义注解
@Target(ElementType.METHOD) // 注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可反射获取
@interface RequirePermission {
// 注解属性(类似方法,可指定默认值)
String value(); // 权限名称(如"admin")
}
// 2. 使用注解标记方法
class UserService {
// 标记需要"admin"权限才能执行
@RequirePermission("admin")
public void deleteUser() {
System.out.println("执行删除用户操作");
}
// 标记需要"user"权限才能执行
@RequirePermission("user")
public void queryUser() {
System.out.println("执行查询用户操作");
}
}
// 3. 通过反射解析注解,实现权限校验
class PermissionChecker {
// 模拟当前用户拥有的权限
private String currentPermission = "admin";
// 执行方法前校验权限
public void executeWithCheck(Object obj, String methodName) throws Exception {
// 获取方法对象
Method method = obj.getClass().getMethod(methodName);
// 判断方法是否有@RequirePermission注解
if (method.isAnnotationPresent(RequirePermission.class)) {
// 获取注解对象
RequirePermission annotation = method.getAnnotation(RequirePermission.class);
// 获取注解的权限值
String requiredPerm = annotation.value();
// 校验权限
if (currentPermission.equals(requiredPerm)) {
method.invoke(obj); // 权限通过,执行方法
} else {
throw new RuntimeException("权限不足,需要:" + requiredPerm);
}
} else {
// 无注解,直接执行
method.invoke(obj);
}
}
}
// 测试
public class CustomAnnotationDemo {
public static void main(String[] args) throws Exception {
UserService userService = new UserService();
PermissionChecker checker = new PermissionChecker();
checker.executeWithCheck(userService, "deleteUser"); // 输出:执行删除用户操作(权限足够)
checker.executeWithCheck(userService, "queryUser"); // 输出:执行查询用户操作(权限足够)
}
}
解析流程:
- 定义注解:用
@Target
和@Retention
指定使用位置和生命周期。 - 使用注解:在目标方法上添加注解,设置属性值。
- 反射解析:通过
method.isAnnotationPresent()
判断是否有注解,method.getAnnotation()
获取注解对象,进而获取属性值并执行逻辑。
四、三者关系与总结
- 泛型:编译时保障类型安全,避免强制转换,是编写健壮代码的基础。
- 反射:运行时动态操作类信息,突破编译时限制,是框架灵活性的核心。
- 注解:通过标记携带元数据,配合反射实现 “标记 - 解析 - 执行”
的逻辑,是简化配置的关键。
三者结合构成了 Java 高级编程的基础 ——
泛型保证类型安全,反射提供动态能力,注解简化元数据携带,共同支撑了
Spring、MyBatis 等主流框架的设计。
更多推荐
所有评论(0)