背景

本文是《Java 后端从小白到大神》修仙系列第十五篇,正式进入Java后端世界,本篇文章主要聊Java基础中的泛型。若想详细学习请点击 首篇博文,我们开始吧。

文章概览

  1. 泛型的核心概念
  2. 泛型类与接口
  3. 泛型方法
  4. 通配符与 PECS 原则
  5. 类型擦除与边界
  6. 泛型的高级应用
  7. 常见陷阱
  8. 代码示例与应用场景

一、泛型的核心概念

1.1 什么是泛型?

泛型(Generics)是 Java 5 引入的特性,允许在类、接口或方法中将类型定义为参数,从而在编译时提供更强的类型检查和代码复用能力。写类 = 造容器,写字段 = 放东西,泛型 = 统一规定放什么东西。

核心思想:将类型本身作为参数传递,在定义时不指定具体类型,而是在使用时动态确定类型。

1.2 泛型的价值

特性 传统方式 泛型方式 优势
类型安全 运行时异常 编译时检查 提前发现类型错误
代码复用 类型强转 + Object 统一类型参数 减少重复代码
可读性 模糊的 Object 类型 明确的类型参数 代码意图清晰
性能 运行时类型转换 编译时类型确定 减少运行时开销

1.3 泛型的基本语法

  1. 泛型格式
  • Box<T>泛型类(有参数,未赋值)
  • T类型参数 或 类型变量
  • Box<String>参数化类型(已给参数赋值)
  • String实际类型参数

为什么 Box<String>参数化类型

原始泛型类(没有传值)

1
class Box<T>

这里 T参数(占位符),就像方法的形参

传入真实类型(给参数赋值)

1
Box<String>

你给参数 T 传入了一个值 String

所以叫: Parameterized Type = 参数化类型
意思就是:
给泛型参数赋了值的类型 → 参数化类型。

  1. 泛型类

泛型类一旦定义了 < T >,类里面的字段、方法参数、返回值,就全部共用同一个类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 泛型类
public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
}
  1. 泛型接口

泛型接口类一旦定义了 < E >,类里面的方法参数、返回值,就全部共用同一个类型。

1
2
3
4
5
6
// 泛型接口
public interface List<E> {
    void add(E element);
    E get(int index);
    boolean isEmpty();
}
  1. 泛型方法

与泛型类和接口基本一致,唯一区别是:前面的 < T > 是 “声明”,后面的 T 是 “使用”,因为这个方法可能不在泛型类里,它自己独立拥有一个泛型,和类无关,所以必须自己声明自己的 T。

1
2
3
4
// 泛型方法
public static <T> T getFirstElement(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

1.4 泛型不擦除规则

泛型不擦除规则

  • 定义层面的泛型(类、接口、父类、方法、字段、参数) → 会保留
    • 保留是指保留在 .class 文件中
    • 目的给编译器看,用于类型检查、方法重写
  • 运行时执行的代码 → 会擦除
    • 而运行时泛型变成 Object 或边界类型
    • 运行时代码是给 JVM 看,执行时无泛型信息
  • 保留 ≠ 运行时存在
    • 保留 = 编译时可见
    • 擦除 = 运行时实际执行

代码示例

  1. 类定义上的泛型
1
public class Box<T> { }
  • 保留信息(给编译器)Box<T>
  • 运行时擦除后(给JVM)Box(T 变 Object)
  1. 接口定义上的泛型
1
public interface List<E> { }
  • 保留信息(给编译器)List<E>
  • 运行时擦除后(给JVM)List(E 变 Object)
  1. 父类 / 父接口上的泛型(最关键)
1
public class MyList extends ArrayList<String> { }
  • 保留信息(给编译器)ArrayList<String>
  • 运行时擦除后(给JVM)ArrayList(泛型不影响类结构)
  1. 方法上的泛型
1
2
3
public <T> T get(T t) {
    return t;
}
  • 保留信息(给编译器)<T> T get(T)
  • 运行时擦除后(给JVM)
1
2
3
public Object get(Object t) {
    return t;
}
  1. 方法参数 / 返回值泛型
1
public static <T> T findMax(List<T> list) { }
  • 保留信息(给编译器)List<T>T
  • 运行时擦除后(给JVM)
1
public static Object findMax(List list) { }
  1. 字段上的泛型
1
2
3
public class Box<T> {
    private T value;
}
  • 保留信息(给编译器)T value
  • 运行时擦除后(给JVM)
1
2
3
public class Box {
    private Object value;
}
  1. 对象实例泛型(一定会被擦除)
1
List<String> list = new ArrayList<>();
  • 给编译器看List<String>
  • 给JVM运行:只有 ArrayList完全没有 <String>

二、泛型类与接口

2.1 泛型类

泛型类是指在类定义时使用类型参数的类。类型参数在类名后面用尖括号 <> 声明。

2.1.1 基本用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * 泛型容器类
 * @param <T> 类型参数,表示容器中元素的类型
 */
public class Box<T> {
    private T content;
    
    /**
     * 构造器
     * @param content 初始内容
     */
    public Box(T content) {
        this.content = content;
    }
    
    /**
     * 获取内容
     * @return 容器中的内容
     */
    public T getContent() {
        return content;
    }
    
    /**
     * 设置内容
     * @param content 新内容
     */
    public void setContent(T content) {
        this.content = content;
    }
}

// 使用示例
public class BoxExample {
    public static void main(String[] args) {
        // 创建存储字符串的盒子
        Box<String> stringBox = new Box<>("Hello, Generics!");
        String message = stringBox.getContent();  // 无需类型转换
        
        // 创建存储整数的盒子
        Box<Integer> intBox = new Box<>(42);
        int value = intBox.getContent();  // 无需类型转换
        
        // 编译时类型检查
        // stringBox.setContent(123);  // 编译错误:类型不匹配
    }
}

2.1.2 多类型参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 键值对泛型类
 * @param <K> 键的类型
 * @param <V> 值的类型
 */
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
}

// 使用示例
Pair<String, Integer> person = new Pair<>("Alice", 30);
Pair<Double, String> product = new Pair<>(99.99, "Laptop");

2.2 泛型接口

泛型接口是在接口定义时使用类型参数的接口。实现泛型接口时,可以指定具体类型或继续使用泛型参数。

2.2.1 基本用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 泛型仓库接口
 * @param <T> 仓库中元素的类型
 */
public interface Repository<T> {
    /**
     * 保存元素
     * @param item 要保存的元素
     */
    void save(T item);
    
    /**
     * 根据 ID 获取元素
     * @param id 元素的 ID
     * @return 找到的元素,未找到返回 null
     */
    T findById(long id);
    
    /**
     * 获取所有元素
     * @return 元素列表
     */
    List<T> findAll();
}

2.2.2 实现泛型接口

方式1:指定具体类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 用户仓库实现
 */
public class UserRepository implements Repository<User> {
    @Override
    public void save(User user) {
        // 保存用户逻辑
    }
    
    @Override
    public User findById(long id) {
        // 根据 ID 查找用户
        return null;
    }
    
    @Override
    public List<User> findAll() {
        // 获取所有用户
        return new ArrayList<>();
    }
}

方式2:继续使用泛型参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * 通用仓库实现
 * @param <T> 仓库中元素的类型
 */
public class GenericRepository<T> implements Repository<T> {
    private Map<Long, T> storage = new HashMap<>();
    private long nextId = 1;
    
    @Override
    public void save(T item) {
        storage.put(nextId++, item);
    }
    
    @Override
    public T findById(long id) {
        return storage.get(id);
    }
    
    @Override
    public List<T> findAll() {
        return new ArrayList<>(storage.values());
    }
}

// 使用
Repository<User> userRepo = new GenericRepository<>();
Repository<Product> productRepo = new GenericRepository<>();

2.2.3 内置泛型接口

Java 标准库中有许多泛型接口,例如:

  • List<E>Set<E>Map<K, V> - 集合接口
  • Comparator<T> - 比较器接口
  • Consumer<T>Supplier<T>Function<T, R> - 函数式接口
  • Optional<T> - 可选值容器

三、泛型方法

3.1 什么是泛型方法?

泛型方法是指在方法声明中使用类型参数的方法,这些类型参数独立于类的类型参数。

3.2 泛型方法的语法

1
2
3
4
5
// 泛型方法语法
public <T> T methodName(T parameter) {
    // 方法体
    return parameter;
}

注意:类型参数 <T> 必须出现在方法返回类型之前。

3.3 泛型方法的优势

  • 灵活性:可以在任何方法中使用,不依赖于类的类型参数
  • 类型推断:编译器可以自动推断类型参数
  • 代码复用:一个方法可以处理多种类型的数据

3.4 代码示例

3.4.1 基本泛型方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class GenericMethodDemo {
    
    /**
     * 打印数组元素
     * @param <T> 数组元素类型
     * @param array 要打印的数组
     */
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    /**
     * 获取数组中的第一个元素
     * @param <T> 数组元素类型
     * @param array 输入数组
     * @return 第一个元素,数组为空返回 null
     */
    public static <T> T getFirstElement(T[] array) {
        return array.length > 0 ? array[0] : null;
    }
    
    public static void main(String[] args) {
        // 打印整数数组
        Integer[] intArray = {1, 2, 3, 4, 5};
        printArray(intArray);  // 输出: 1 2 3 4 5
        
        // 打印字符串数组
        String[] stringArray = {"Hello", "World", "Java"};
        printArray(stringArray);  // 输出: Hello World Java
        
        // 获取第一个元素
        Integer firstInt = getFirstElement(intArray);  // 1
        String firstString = getFirstElement(stringArray);  // "Hello"
    }
}

3.4.2 静态泛型方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Box<T> {
    private T content;
    
    // 静态泛型方法 - 必须单独声明类型参数
    public static <U> Box<U> create(U content) {
        return new Box<>(content);
    }
    
    public Box(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// 使用
Box<String> stringBox = Box.create("Hello");
Box<Integer> intBox = Box.create(42);

3.4.3 类型参数约束

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class GenericMethodWithBounds {
    
    /**
     * 比较两个对象的大小并返回较大的
     * @param <T> 实现了 Comparable 接口的类型
     * @param a 第一个对象
     * @param b 第二个对象
     * @return 较大的对象
     */
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    
    public static void main(String[] args) {
        System.out.println(max(10, 20));  // 20
        System.out.println(max("apple", "banana"));  // "banana"
        
        // max(new Object(), new Object());  // 编译错误:Object 未实现 Comparable
    }
}

四、通配符与 PECS 原则

4.1 通配符的类型

Java 泛型中的通配符(Wildcard)用 ? 表示,主要有三种形式:

  1. 无界通配符? - 表示任何类型
  2. 上界通配符? extends T - 表示 T 或 T 的子类型
  3. 下界通配符? super T - 表示 T 或 T 的父类型

4.2 无界通配符

用途:处理未知类型的集合,适用于只读取不修改的场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UnboundedWildcardDemo {
    
    /**
     * 打印任何类型的列表
     * @param list 待打印的列表
     */
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<String> stringList = Arrays.asList("a", "b", "c");
        
        printList(intList);     // 输出: 1 2 3
        printList(stringList);  // 输出: a b c
        
        // 注意:不能添加元素(除了 null)
        // list.add("test");  // 编译错误
    }
}

4.3 上界通配符

用途? extends 类型 → 只读不写。因为 ? extends Number 是未知类型,可能是 Integer、可能是 Integer、可能是 Integer,编译器根本不敢让你 add 任何东西!编译器根本不敢让你 add 任何东西!? extends T :只读,不能 add /set

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UpperBoundedWildcardDemo {
    
    /**
     * 计算数字列表的总和
     * @param numbers 数字列表
     * @return 总和
     */
    public static double sum(List<? extends Number> numbers) {
        double total = 0.0;
        for (Number number : numbers) {
            total += number.doubleValue();
        }
        return total;
    }
    
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
        List<Long> longList = Arrays.asList(1L, 2L, 3L);
        
        System.out.println(sum(intList));     // 6.0
        System.out.println(sum(doubleList));  // 6.6
        System.out.println(sum(longList));    // 6.0
        
        // 注意:不能添加元素
        // numbers.add(4);  // 编译错误
    }
}

4.4 下界通配符

用途? super 类型 → 只写不读。因为,至少是 Number 或其父类,所以放 Number 子类 一定安全,编译器不会报错,但不能安全读。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LowerBoundedWildcardDemo {
    
    /**
     * 向列表添加数字
     * @param list 数字列表
     */
    public static void addNumbers(List<? super Number> list) {
        list.add(1);      // Integer
        list.add(1.1);    // Double
        list.add(1L);     // Long
        list.add(1.0f);   // Float
    }
    
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();
        
        addNumbers(numberList);
        addNumbers(objectList);
        
        System.out.println(numberList);  // [1, 1.1, 1, 1.0]
        System.out.println(objectList);  // [1, 1.1, 1, 1.0]
        
        // List<Integer> intList = new ArrayList<>();
        // addNumbers(intList);  // 编译错误:Integer 不是 Number 的父类型
    }
}

4.5 PECS 原则

PECSProducer Extends, Consumer Super 的缩写,是使用通配符的核心原则:

  • Producer(生产者):当你需要从集合中读取元素时,使用 <? extends T>

    • 集合是数据的生产者,你从它那里获取数据
    • 例如:List<? extends Number> 可以读取 Number 类型的数据
  • Consumer(消费者):当你需要向集合中写入元素时,使用 <? super T>

    • 集合是数据的消费者,你向它写入数据
    • 例如:List<? super Integer> 可以写入 Integer 类型的数据

4.5.1 PECS 原则应用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class PECSDemo {
    
    /**
     * 合并两个列表
     * @param src 源列表(生产者)
     * @param dest 目标列表(消费者)
     */
    public static void mergeLists(List<? extends Number> src, List<? super Number> dest) {
        dest.addAll(src);
    }
    
    public static void main(String[] args) {
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(4.4, 5.5);
        List<Number> numberList = new ArrayList<>();
        List<Object> objectList = new ArrayList<>();
        
        // 合并整数列表到 Number 列表
        mergeLists(intList, numberList);
        System.out.println(numberList);  // [1, 2, 3]
        
        // 合并双精度列表到 Object 列表
        mergeLists(doubleList, objectList);
        System.out.println(objectList);  // [4.4, 5.5]
    }
}

五、类型擦除与边界

5.1 类型擦除(Type Erasure)

类型擦除 Java 泛型 = 给编译器看的假类型,运行时 = 全部变成 Object / 边界类型。是 Java 泛型的核心机制,编译器在编译时会 擦除 泛型类型信息,将泛型代码转换为原始类型代码。

5.1.1 擦除规则

  • 擦除 = 去掉 < >
  • 无界 → 用 Object
  • 有界 → 用 extends 后面的那个类型
  • 最终都变成 原始类型(不带尖括号)

无界类型参数

  • 无界类型参数:编译后泛型信息被擦除,类型参数替换为 Object
    • List<T> → 原始类型 List,内部按 Object 处理
    • Box<T> → 原始类型 Box,内部按 Object 处理

有界类型参数

  • 有界类型参数:编译后泛型信息被擦除,类型参数替换为上边界类型
    • 不管是 super 还是 extends,擦除后都变成 Object
    • List<T extends Number> → 原始类型 List,内部 T 按 Number 处理
    • Box<T extends Comparable<T>> → 原始类型 Box,内部 T 按 Comparable 处理

5.1.2 类型擦除示例代码

  1. stringList 和 intList 运行时类型相同
1
2
3
4
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true

为什么 stringList 和 intList 运行时类型相同,因为泛型类型被擦除了!运行时它们都是:

1
2
ArrayList
ArrayList

类型一样,所以 == 结果是 true。

  1. 类型擦除证明
1
2
3
// 通过反射添加不同类型的元素
Method addMethod = stringList.getClass().getMethod("add", Object.class);
addMethod.invoke(stringList, 123);  // 运行时成功

明明定义的是

1
List<String>

按理说只能加字符串!

但运行时它变成了

1
List

也就是 List< Object >

所以,可以往 String 列表里加数字 123!编译器拦得住,但运行时拦不住! 这就是类型擦除最铁的证据!

5.2 类型边界

类型边界 用于限制泛型类型参数的范围,使用 extendssuper 关键字。

5.2.1 单边界

1
2
3
4
5
6
7
8
// T 必须是 Number 或其子类型
public class NumberBox<T extends Number> {
    private T value;
    
    public double getDoubleValue() {
        return value.doubleValue();  // 可以安全调用 Number 的方法
    }
}

5.2.2 多边界

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// T 必须同时实现 Comparable 和 Serializable 接口
public class ComparableBox<T extends Comparable<T> & Serializable> {
    private T value;
    
    public int compareTo(T other) {
        return value.compareTo(other);  // 可以调用 compareTo 方法
    }
    
    public void serialize() throws IOException {
        // 可以序列化
    }
}

5.2.3 类型边界的应用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class BoundedGenericsDemo {

    /**
     * 查找列表中的最大值
     * @param <T> 实现了 Comparable 接口的类型
     * @param list 元素列表
     * @return 最大值
     */
    public static <T extends Comparable<T>> T findMax(List<T> list) {
        if (list == null || list.isEmpty()) {
            throw new IllegalArgumentException("List cannot be empty");
        }

        T max = list.get(0);
        for (T element : list) {
            if (element.compareTo(max) > 0) {
                max = element;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);
        List<String> words = Arrays.asList("apple", "banana", "cherry");

        System.out.println(findMax(numbers));  // 9
        System.out.println(findMax(words));    // cherry
    }
}

代码解释

1
<T extends Comparable<T>>

含义:

  • <T>:声明泛型类型
  • extends Comparable<T>:对 T 做类型边界限制
  • 最终意思:

    T 必须是实现了 Comparable<T> 接口的类型

只有满足这个条件,T 才拥有 compareTo 方法,才能进行大小比较。

这段代码如何体现“类型边界”?

  1. 限制泛型必须具备比较能力 通过 <T extends Comparable<T>> 强制要求:
  • 传入的类型必须实现 Comparable
  • 只有这样才能调用 compareTo 进行大小比较

如果不加这个边界,编译器会报错,因为它无法确定 T 是否有比较方法。

  1. 保证方法内部逻辑合法 代码中用到:
1
element.compareTo(max);
  • compareToComparable 接口中的方法
  • 类型边界保证了该方法一定存在
  • 编译器可以安全放行,不会出现类型不安全调用
  1. 约束参数与返回值类型
1
public static <T extends Comparable<T>> T findMax(List<T> list)
  • 参数 List<T>:列表元素必须是可比较类型
  • 返回值 T:返回的最大值也必须是同一可比较类型
  • 整个方法的输入、输出、逻辑共用同一个受约束的泛型 T
  1. 实际使用时自动类型匹配 在 main 中:
1
2
3
4
5
List<Integer> numbers = ...;
List<String> words = ...;

findMax(numbers);
findMax(words);
  • Integer 实现了 Comparable<Integer>
  • String 实现了 Comparable<String>
  • 都满足边界约束,因此可以正常调用

类型边界的作用总结

  1. 对泛型类型做能力限制,只允许符合条件的类型传入
  2. 让编译器提前校验方法调用的合法性(如 compareTo
  3. 既保持泛型通用性,又保证类型安全
  4. 是泛型方法实现“通用算法”(如求最大、排序)的核心手段

六、泛型的高级应用

6.1 泛型与反射

由于类型擦除,泛型类型信息在运行时不可用,但可以通过反射来处理。

6.1.1 获取泛型类型信息

正常new对象 vs 匿名内部类

  • 正常 new 对象(没有大括号)

    • 类名 对象名 = new 类名();
    • List< String > list1 = new ArrayList< String >();
  • 匿名内部类(有大括号)

    • 类名 对象名 = new 类名() { … };
    • List< String> stringList = new ArrayList< String >() {};
    • 含义:创建一个“没有名字的子类”,继承自后面的类(这里是ArrayList< String >),大括号里可以重写方法、添加字段(代码中未重写,仅创建子类)。
  • 方法介绍

    • Type = 代表任何类型
    • ParameterizedType = 带 <> 的泛型类型
    • getGenericSuperclass() = 获取带泛型的父类
    • getActualTypeArguments() = 获取真实的泛型类型(如 String)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

public class GenericTypeDemo {

    public static void main(String[] args) {
        // 1. 创建匿名内部类(核心!结合前置知识)
        // 本质:创建ArrayList<String>的子类(无类名),再创建子类对象
        // 目的:绕过类型擦除,把泛型<String>保存在父类(ArrayList<String>)上
        List<String> stringList = new ArrayList<String>() {};
        
        // 2. 获取带泛型信息的父类
        // stringList.getClass():获取当前对象的真实类型(匿名内部类的Class)
        // getGenericSuperclass():获取“带泛型信息”的父类(这里就是ArrayList<String>)
        // 为什么要拿父类?因为泛型<String>保存在父类上,匿名内部类本身没有泛型
        Type superType = stringList.getClass().getGenericSuperclass();
        
        // 3. 判断父类是否是“带尖括号的泛型类型”(ParameterizedType)
        // 只有带<>的类型(比如ArrayList<String>)才是ParameterizedType
        if (superType instanceof ParameterizedType) {
            // 4. 强转成ParameterizedType,才能获取里面的泛型信息
            ParameterizedType parameterizedType = (ParameterizedType) superType;
            
            // 5. 核心方法:获取泛型尖括号里的真实类型(返回数组,因为泛型可能有多个,比如Map<K,V>)
            // 这里ArrayList<String>只有一个泛型,所以数组长度为1,里面是String类型
            Type[] typeArguments = parameterizedType.getActualTypeArguments();
            
            // 6. 遍历输出真实的泛型类型(这里只会输出String)
            for (Type typeArgument : typeArguments) {
                // 输出结果:Generic type: class java.lang.String
                System.out.println("Generic type: " + typeArgument);  
            }
        }
    }
}

6.1.2 泛型工厂方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GenericFactory {
    
    /**
     * 创建泛型类型的实例
     * @param <T> 要创建的类型
     * @param clazz 类型的 Class 对象
     * @return 类型的实例
     */
    public static <T> T createInstance(Class<T> clazz) {
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Failed to create instance", e);
        }
    }
    
    public static void main(String[] args) {
        String str = createInstance(String.class);
        Integer integer = createInstance(Integer.class);
        List<?> list = createInstance(ArrayList.class);
    }
}

6.2 泛型与序列化

泛型在序列化是要注意以下内容:

  1. 泛型类要实现序列化。如 Box 实现了 Serializable。序列化时,Box< String > 会被类型擦除为 Box(原始类型),JVM 不会记录泛型 < String > 的信息,只会序列化 content 的实际值(“Hello, Generics!")和 Box 类的结构;
  2. 泛型类实现 Serializable,且泛型对应的实际类型(T的具体类型)也要实现 Serializable。示例中 T 是 String(String 实现了 Serializable),所以能正常序列化;
  3. 反序列化时,JVM 只能还原出 Box 对象,无法知道原来的泛型是 String,所以不能直接写 Box< String > deserializedBox = (Box< String >) ois.readObject()。要用通配符 Box< ? > deserializedBox = (Box) ois.readObject() 接收,后续使用时要做安全的类型判断 deserializedBox.getContent() instanceof String,避免类型转换异常。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class GenericSerializationDemo {
    
    public static void main(String[] args) throws Exception {
        // 1. 创建泛型对象(指定泛型为String)
        Box<String> box = new Box<>("Hello, Generics!");
        
        // 2. 序列化:将对象转为字节流(此时泛型信息会受类型擦除影响)
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(box); // 序列化时,Box<String> 的泛型 <String> 会被擦除,仅保留Box和content的实际值
        oos.close();
        
        // 3. 反序列化:将字节流转回对象(关键问题在这里!)
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bis);
        // 反序列化时,无法直接拿到泛型<String>,只能用通配符?接收(或强转有风险)
        Box<?> deserializedBox = (Box<?>) ois.readObject();
        ois.close();
        
        // 看似正常输出,但隐藏类型安全隐患:未对deserializedBox.getContent()做类型判断
        // 若反序列化后content实际类型不是String,直接使用会抛出ClassCastException
        // 真正危险的是里面的 content 不知道是什么类型,所以 deserializedBox.getContent() instanceof String ,安全的类型检查
        System.out.println(deserializedBox.getContent());  // Hello, Generics!
    }
    
    // 可序列化的泛型类(必须实现Serializable接口,否则无法序列化)
    static class Box<T> implements Serializable {
        // 注意:泛型字段content,序列化时会保留其「实际值」,但不保留「泛型类型T的具体类型」
        private T content;
        
        public Box(T content) {
            this.content = content;
        }
        
        public T getContent() {
            return content;
        }
    }
}

6.3 泛型与桥接方法

编译器为了保持泛型类型的兼容性,会生成桥接方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class BridgeMethodDemo {
    public static void main(String[] args) {
        // 1. 创建子类对象(正常)
        ChildStringList child = new ChildStringList();

        // 2. 赋值给 父类引用
        ParentList<Object> parentRef = child; // 父类引用指向子类对象

        // 3. 尝试添加 Object —— 编译不报错,运行报错!
        parentRef.add(new Object());
    }

    // 父类(泛型类)
    static class ParentList<T> {
        public void add(T item) {
            System.out.println("Parent.add(" + item + ")");
        }
    }

    // 子类:固定泛型为 String
    static class ChildStringList extends ParentList<String> {
        @Override
        public void add(String item) {
            System.out.println("Child.add(" + item + ")");
        }
    }
}

代码分析

第一步:先看代码结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 父类:泛型类
class ParentList<T> {
    public void add(T item) { ... }
}

// 子类:继承父类,指定 T = String
class ChildStringList extends ParentList<String> {
    @Override
    public void add(String item) { ... }
}

你以为:

  • 父类:add(T)
  • 子类:add(String)
    正常重写 ?

但是!类型擦除来了!
擦除后变成:

1
2
3
4
5
6
7
class ParentList {
    public void add(Object item) { ... }  // T 被擦成 Object
}

class ChildStringList extends ParentList {
    public void add(String item) { ... }
}

问题来了!

父方法:add(Object)
子方法:add(String)

这在 Java 里不算重写!是重载!

那你写的 @Override 就会报错!

第二步:编译器偷偷干了什么?

编译器自动生成一个【桥接方法】来假装重写!

编译器会给子类强行加一个方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ChildStringList extends ParentList {
    
    // 你写的方法
    public void add(String item) { ... }

    // 【编译器自动生成的桥接方法】
    @Override
    public void add(Object item) {
         // 内部强转 String,再调用你写的方法
        this.add( (String) item );
    }
}

桥接方法的作用:

  1. 满足重写(匹配父类 add(Object)
  2. 内部强转类型,保证安全
  3. 对程序员完全隐藏

第三步:隐藏的坑!(代码演示的就是这个)

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    ChildStringList child = new ChildStringList();
    
    // 把子类赋值给父类引用(泛型被擦除)
    List<Object> objList = child;
    
    // 编译器不报错!
    objList.add(new Object()); 
}

为什么编译器不报错?
因为 objList 看起来是 ParentList<Object> 调用的是桥接方法 add(Object)

运行时发生了什么?
桥接方法执行:

1
2
3
public void add(Object item) {
    this.add( (String) item );  // 强转失败!
}

七、常见陷阱

7.1 常见陷阱

7.1.1 泛型数组创建

1
2
3
4
5
6
7
8
9
// 错误:不能直接创建泛型数组
// T[] array = new T[10];

// 正确:强转 Object 数组 或 通配符
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[10];

// 推荐:直接使用集合
List<T> list = new ArrayList<>();

7.1.2 泛型类型实例化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 错误:不能实例化泛型类型
// T instance = new T();

// 正确:使用工厂方法或反射
public <T> T createInstance(Class<T> clazz) {
    try {
        return clazz.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

7.1.3 静态字段与泛型

1
2
3
4
5
6
7
public class GenericStaticField<T> {
    // 错误:静态字段不能使用泛型类型参数
    // private static T staticValue;
    
    // 正确:使用具体类型
    private static Object staticValue;
}

7.1.4 泛型与异常

1
2
3
4
5
// 错误:不能抛出或捕获泛型异常
// public <T extends Exception> void method() throws T {}

// 正确:使用具体的异常类型
public void method() throws Exception {}

7.1.5 泛型与 instanceof

1
2
3
4
5
6
7
8
// 错误:不能使用 instanceof 检查泛型类型
// if (obj instanceof List<String>) {}

// 正确:使用无界通配符
if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    // 处理列表
}

八、代码示例与应用场景

8.1 泛型工具类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
 * 泛型工具类
 */
public class GenericUtils {
    
    /**
     * 安全地将列表转换为数组
     * @param <T> 元素类型
     * @param list 输入列表
     * @param array 用于确定类型的数组
     * @return 转换后的数组
     */
    @SuppressWarnings("unchecked")
    public static <T> T[] toArray(List<T> list, T[] array) {
        return list.toArray(array);
    }
    
    /**
     * 创建类型安全的单例集合
     * @param <T> 元素类型
     * @param item 单个元素
     * @return 只包含一个元素的列表
     */
    public static <T> List<T> singletonList(T item) {
        return Collections.singletonList(item);
    }
    
    /**
     * 安全地获取列表元素
     * @param <T> 元素类型
     * @param list 输入列表
     * @param index 索引
     * @param defaultValue 默认值
     * @return 元素值或默认值
     */
    public static <T> T getOrDefault(List<T> list, int index, T defaultValue) {
        return index >= 0 && index < list.size() ? list.get(index) : defaultValue;
    }
}

8.2 泛型缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * 泛型缓存
 * @param <K> 键类型
 * @param <V> 值类型
 */
public class GenericCache<K, V> {
    private final Map<K, V> cache = new ConcurrentHashMap<>();
    
    /**
     * 获取缓存值
     * @param key 键
     * @return 值,不存在返回 null
     */
    public V get(K key) {
        return cache.get(key);
    }
    
    /**
     * 放入缓存
     * @param key 键
     * @param value 值
     */
    public void put(K key, V value) {
        cache.put(key, value);
    }
    
    /**
     * 移除缓存
     * @param key 键
     * @return 被移除的值
     */
    public V remove(K key) {
        return cache.remove(key);
    }
    
    /**
     * 清空缓存
     */
    public void clear() {
        cache.clear();
    }
}

// 使用
GenericCache<String, User> userCache = new GenericCache<>();
userCache.put("alice", new User("Alice", 30));
User user = userCache.get("alice");

8.3 泛型构建器模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * 泛型构建器
 * @param <T> 构建的目标类型
 */
public class GenericBuilder<T> {
    // 构造器:一个能制造 T 对象的工厂,Supplier<T> 是 Java 8 的函数式接口,包含一个get()方法
    private final Supplier<T> instantiator;
    // 存放所有“属性设置逻辑”,key=属性名,value=设置方法
    private final Map<String, Function<Object, Object>> attributeSetters = new HashMap<>();
    
    // 私有构造方法
    private GenericBuilder(Supplier<T> instantiator) {
        this.instantiator = instantiator;
    }
    
    // 静态创建方法
    public static <T> GenericBuilder<T> of(Supplier<T> instantiator) {
        return new GenericBuilder<>(instantiator);
    }
    
    // with 方法(核心!链式赋值)
    public <V> GenericBuilder<T> with(
        String propertyName ,  // 属性名
        Function<T, Function<V, T>> setter, // setter 方法
        V value // 要设置的值
        ) {
        attributeSetters.put(propertyName, v -> setter.apply(instantiator.get()).apply((V) v));
        return this;
    }
    
    // 最终 build 的时候,调用工厂造对象
    public T build() {
        // 1. 创建对象
        T instance = instantiator.get();
        // 2. 执行所有 set 方法
        attributeSetters.forEach((propertyName, setter) -> {
            try {
                // 反射获取 setter 方法
                Method method = instance.getClass().getMethod(
                    "set" + capitalize(propertyName),  // setName
                    setter.getClass()
                );
                method.invoke(instance, attributeSetters.get(propertyName));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
        // 3. 返回完整对象
        return instance;
    }

    // 辅助方法:首字母大写
    private String capitalize(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }
}

// 使用示例
User user = GenericBuilder.of(User::new)
    .with("name", User::setName, "Alice")
    .with("age", User::setAge, 30)
    .build();

九、总结

  1. 泛型本质:将类型作为参数,实现代码复用与编译期类型安全。
  2. 类型擦除:泛型信息仅存在编译期,运行时会被擦除,无法用 instanceof 判断具体泛型类型(如 List<String>)。
  3. 通配符:使用 ?? extends T? super T 实现灵活兼容。
  4. PECS 原则:读取(生产者)用 extends,写入(消费者)用 super
  5. 类型边界:通过 <T extends 类/接口> 限制泛型范围,增强类型检查。
  6. 禁止使用原生类型(如 List),必须声明具体泛型(如 List<String>),否则失去类型检查。
  7. 泛型类如需序列化,要实现 Serializable,内部字段也需可序列化。
  8. 反序列化泛型对象优先使用 ? 通配符,避免强制转换;反序列化后需判断类型,防止 ClassCastException
  9. 泛型声明尽量精简,只在需要通用、类型安全的场景使用。
  10. 使用有意义的类型参数名,合理利用 <> 钻石运算符简化代码。

泛型是 Java 中非常强大的特性,掌握好泛型可以写出更加灵活、安全、可维护的代码。通过本文的学习,相信你已经对泛型有了更深入的理解,可以在实际开发中灵活运用泛型来解决问题。