LESSON 07 · 泛型 + 反射 + 注解

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> 区别?
  1. List<Object> = 只能装 Object 及其子类(实际上能装任何东西)
  2. List<?> = 任意类型的 List,但不能 add(除 null)
  3. 前者的"任意"是写时宽,后者的"任意"是读时
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类的"蓝图"forNamegetNamegetDeclaredFields
Field类的字段getsetsetAccessible
Method类的方法invokegetParameters
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 个面试常问点:
  1. List<String>.class 合法吗?→ 不合法,泛型不能用于 .class(被擦除了)
  2. 反射能拿到泛型类型吗?→ 能,用 getGenericSuperclass() / getGenericReturnType(),但比直接拿 Class 麻烦
  3. 泛型数组 new List<String>[10] 合法吗?→ 不合法,但 new List[10] 然后强转可以(编译器警告 unchecked)

反射的 3 个性能代价

  1. :反射调用比直接调用慢 10-100 倍
  2. 不安全:能调 private,破坏封装
  3. 难维护:编译期不报错,运行才挂
实战建议:业务代码别用反射,框架内部用。99% 的场景有更直接的写法。

你的作业(分步走)

  1. 写泛型类:写个 Pair<K, V>,有两个字段 keyvalue,提供 getter / setter / 构造器;用它存 ("age", 25) 并打印
  2. 通配符:写两个方法,一个 printAll(List<? extends Number>)(只读),一个 addZeros(List<? super Integer>)(只写),对比区别
  3. 反射读字段:写代码用反射读出 String.class.getDeclaredFields() 里的所有字段(你会发现 value 字段是 private final char[]
  4. 反射调方法:写代码用反射调用 "hello".getClass().getMethod("length").invoke("hello"),应该返回 5
  5. 自定义注解 + 反射读:定义 @MyTag 注解(保留到运行时,能贴在方法上),写个类的方法贴上 @MyTag("test"),再用反射读出 value() 的值
  6. 类型擦除验证:写代码 new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass(),打印结果——确认是 true