Java 三大高级特性
泛型 / 反射 / 注解
这节啃 Java 里 3 个"语法层面的高级特性":泛型(类型参数化)、反射(运行时看类的结构)、注解(给代码加元数据)。Spring 全家桶、JUnit、Jackson 等等框架都重度依赖这 3 个。
学完这节:你能写自己的泛型类、能用反射在运行时读类的字段和方法、知道
@Override / @Deprecated 背后的原理、能看懂 Spring 的 @Autowired 是怎么"自动注入"的。
1
泛型:把类型当参数传
💡 动机:List 不写泛型就是 List
没有泛型的时候,集合装的是 Object,取出来必须强转,写错就 ClassCastException:
// ❌ 不写泛型:编译器不报错,运行炸
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译过
String s = (String) list.get(1); // 运行 ClassCastException
// ✅ 写泛型:编译时就拦住
List<String> list = new ArrayList<String>();
list.add("hello");
list.add(123); // 编译报错!类型不匹配
写自己的泛型类
// 泛型类:<T> 是"类型参数",实例化时指定
public class Box<T> {
private T content;
public void put(T item) { this.content = item; }
public T get() { return content; }
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.put("hello");
String s = stringBox.get(); // 不需要强转
Box<Integer> intBox = new Box<>();
intBox.put(42);
int n = intBox.get(); // 自动拆箱成 int
泛型命名约定:
T= Type(最常用)E= Element(集合里用)K/V= Key / Value(Map 里用)?= 通配符("任意类型")
2
通配符与边界 ⭐ 面试常问
💡 动机:方法参数想接受"任何类型的 List",但又不能随便 add
| 写法 | 含义 | 能做什么 |
|---|---|---|
?(无界) |
任意类型 | 只能读,不能 add |
? extends T(上界) |
T 或 T 的子类 | 只能读 T,不能 add |
? super T(下界) |
T 或 T 的父类 | 可以 add T,读出来是 Object |
// PECS 原则:Producer Extends, Consumer Super
public void printList(List<? extends Number> list) { // 读(producer)
for (Number n : list) {
System.out.println(n);
}
}
public void addIntegers(List<? super Integer> list) { // 写(consumer)
list.add(1);
list.add(2);
}
⭐ 面试题:List<?> 和 List<Object> 区别?
List<Object>= 只能装 Object 及其子类(实际上能装任何东西)List<?>= 任意类型的 List,但不能 add(除 null)- 前者的"任意"是写时宽,后者的"任意"是读时宽
3
反射:在运行时"看"类的结构
💡 动机:正常代码编译时就确定类型,反射让你运行时才"看"
反射 = 在程序运行期间,动态地获取类的信息(字段、方法、构造器),甚至调用它们。
public class Person {
private String name = "张三";
public int age = 25;
public void sayHi() {
System.out.println("Hi, I'm " + name);
}
}
// 反射的 3 步:获取 Class → 创建实例 → 调用方法
Class<?> clazz = Class.forName("Person");
Object obj = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getDeclaredMethod("sayHi");
m.invoke(obj); // → "Hi, I'm 张三"
// 还能"强闯" private 字段
Field f = clazz.getDeclaredField("name");
f.setAccessible(true); // 关闭访问检查
System.out.println(f.get(obj));
反射的 3 个核心 API
| 类 | 作用 | 常见方法 |
|---|---|---|
Class | 类的"蓝图" | forName、getName、getDeclaredFields |
Field | 类的字段 | get、set、setAccessible |
Method | 类的方法 | invoke、getParameters |
Constructor | 类的构造器 | newInstance |
⭐ 反射的"为什么":
- Spring 启动时扫描所有
@Component类,反射创建实例注入容器 - Jackson / Gson 解析 JSON 时,反射读取字段填充到对象
- JUnit 调用
@Test方法,反射调用方法 - MyBatis 查数据库时,反射把 ResultSet 字段映射到 Java 对象
4
注解:给代码加"标签"
💡 动机:框架怎么知道哪个类要扫描、哪个方法是测试?——靠注解
注解(Annotation)= 贴在代码上的标签,本身不做任何事,但框架能读到它,然后做相应处理。
// JDK 自带的 3 个核心注解
@Override // 告诉编译器:我在重写父类方法
@Deprecated // 告诉别人:这个方法已废弃,别用
@SuppressWarnings("unchecked") // 告诉编译器:别报这个警告
// 业务里常见注解(框架的)
@SpringBootApplication // Spring Boot 启动类
@RestController // REST 控制器
@Autowired // 自动注入依赖
@Test // JUnit 测试方法
@Override // 方法重写
自定义注解(5 个元注解)
// 1. 定义注解
@Retention(RetentionPolicy.RUNTIME) // 注解保留到运行时
@Target(ElementType.METHOD) // 注解能贴在哪里
public @interface MyTest {
String value() default ""; // 注解的属性
}
// 2. 使用注解
public class UserService {
@MyTest("登录场景")
public void login() { ... }
}
// 3. 框架通过反射读取注解
Method m = UserService.class.getMethod("login");
MyTest ann = m.getAnnotation(MyTest.class);
System.out.println(ann.value()); // → "登录场景"
4 个关键元注解(meta-annotation):
@Retention= 注解保留到什么时候(SOURCE / CLASS / RUNTIME)@Target= 注解能贴在哪里(METHOD / FIELD / TYPE / PARAMETER...)@Documented= 是否包含在 Javadoc 里@Inherited= 子类是否继承父类的注解
5
实战场景:模拟 Spring 的 @Autowired ⭐ 面试加分项
💡 动机:面试问"反射有什么用"时,能给个真实例子
手写一个简化版"依赖注入"——只要类里有 @Inject 字段,自动 new 一个塞进去:
// 1. 自定义 @Inject 注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {}
// 2. 业务类
public class UserService {
@Inject
private UserRepository repo; // 字段没初始化
}
// 3. 简化版 IOC 容器
public class Container {
public static <T> T createInstance(Class<T> clazz) throws Exception {
T obj = clazz.getDeclaredConstructor().newInstance();
// 扫描所有字段
for (Field f : clazz.getDeclaredFields()) {
if (f.isAnnotationPresent(Inject.class)) {
Object dep = createInstance(f.getType()); // 递归创建依赖
f.setAccessible(true);
f.set(obj, dep); // 反射赋值
}
}
return obj;
}
}
// 4. 使用
UserService service = Container.createInstance(UserService.class);
// repo 字段已被自动注入!
⭐ 这就是 Spring 的核心原理:启动时扫所有
@Component 类,反射创建实例,扫 @Autowired 字段,反射注入。你写的代码就是 Spring 的"迷你版"。
面试时能把这个讲清楚,直接加分。
6
泛型擦除 + 反射的"坑" ⭐ 面试
💡 动机:面试必问——泛型信息在运行时还在吗?
⭐ 类型擦除(Type Erasure)
Java 泛型是编译期检查,运行时会"擦除"成原始类型:
// 编译后:
List<String> 和 List<Integer> → 都变成 List(原始类型)
// 反射看到的:
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass() == intList.getClass()); // → true!
⭐ 3 个面试常问点:
List<String>.class合法吗?→ 不合法,泛型不能用于 .class(被擦除了)- 反射能拿到泛型类型吗?→ 能,用
getGenericSuperclass()/getGenericReturnType(),但比直接拿Class麻烦 - 泛型数组
new List<String>[10]合法吗?→ 不合法,但new List[10]然后强转可以(编译器警告 unchecked)
反射的 3 个性能代价
- 慢:反射调用比直接调用慢 10-100 倍
- 不安全:能调 private,破坏封装
- 难维护:编译期不报错,运行才挂
实战建议:业务代码别用反射,框架内部用。99% 的场景有更直接的写法。
你的作业(分步走)
- 写泛型类:写个
Pair<K, V>,有两个字段key和value,提供 getter / setter / 构造器;用它存("age", 25)并打印 - 通配符:写两个方法,一个
printAll(List<? extends Number>)(只读),一个addZeros(List<? super Integer>)(只写),对比区别 - 反射读字段:写代码用反射读出
String.class.getDeclaredFields()里的所有字段(你会发现value字段是private final char[]) - 反射调方法:写代码用反射调用
"hello".getClass().getMethod("length").invoke("hello"),应该返回 5 - 自定义注解 + 反射读:定义
@MyTag注解(保留到运行时,能贴在方法上),写个类的方法贴上@MyTag("test"),再用反射读出value()的值 - 类型擦除验证:写代码
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass(),打印结果——确认是 true