背景
本文是《Java 后端从小白到大神》修仙系列第十五篇,正式进入Java后端世界,本篇文章主要聊Java基础中的泛型。若想详细学习请点击 首篇博文,我们开始吧。
文章概览
- 泛型的核心概念
- 泛型类与接口
- 泛型方法
- 通配符与 PECS 原则
- 类型擦除与边界
- 泛型的高级应用
- 常见陷阱
- 代码示例与应用场景
一、泛型的核心概念
1.1 什么是泛型?
泛型(Generics)是 Java 5 引入的特性,允许在类、接口或方法中将类型定义为参数,从而在编译时提供更强的类型检查和代码复用能力。写类 = 造容器,写字段 = 放东西,泛型 = 统一规定放什么东西。
核心思想:将类型本身作为参数传递,在定义时不指定具体类型,而是在使用时动态确定类型。
1.2 泛型的价值
| 特性 |
传统方式 |
泛型方式 |
优势 |
| 类型安全 |
运行时异常 |
编译时检查 |
提前发现类型错误 |
| 代码复用 |
类型强转 + Object |
统一类型参数 |
减少重复代码 |
| 可读性 |
模糊的 Object 类型 |
明确的类型参数 |
代码意图清晰 |
| 性能 |
运行时类型转换 |
编译时类型确定 |
减少运行时开销 |
1.3 泛型的基本语法
- 泛型格式
Box<T> → 泛型类(有参数,未赋值)
T → 类型参数 或 类型变量
Box<String> → 参数化类型(已给参数赋值)
String → 实际类型参数
为什么 Box<String> 叫 参数化类型?
原始泛型类(没有传值)
这里 T 是参数(占位符),就像方法的形参。
传入真实类型(给参数赋值)
你给参数 T 传入了一个值 String。
所以叫:
Parameterized Type = 参数化类型
意思就是:
给泛型参数赋了值的类型 → 参数化类型。
- 泛型类
泛型类一旦定义了 < 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;
}
}
|
- 泛型接口
泛型接口类一旦定义了 < E >,类里面的方法参数、返回值,就全部共用同一个类型。
1
2
3
4
5
6
|
// 泛型接口
public interface List<E> {
void add(E element);
E get(int index);
boolean isEmpty();
}
|
- 泛型方法
与泛型类和接口基本一致,唯一区别是:前面的 < 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
|
public class Box<T> { }
|
- 保留信息(给编译器):
Box<T>
- 运行时擦除后(给JVM):
Box(T 变 Object)
- 接口定义上的泛型
1
|
public interface List<E> { }
|
- 保留信息(给编译器):
List<E>
- 运行时擦除后(给JVM):
List(E 变 Object)
- 父类 / 父接口上的泛型(最关键)
1
|
public class MyList extends ArrayList<String> { }
|
- 保留信息(给编译器):
ArrayList<String>
- 运行时擦除后(给JVM):
ArrayList(泛型不影响类结构)
- 方法上的泛型
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
|
public static <T> T findMax(List<T> list) { }
|
- 保留信息(给编译器):
List<T>、T
- 运行时擦除后(给JVM):
1
|
public static Object findMax(List list) { }
|
- 字段上的泛型
1
2
3
|
public class Box<T> {
private T value;
}
|
- 保留信息(给编译器):
T value
- 运行时擦除后(给JVM):
1
2
3
|
public class Box {
private Object value;
}
|
- 对象实例泛型(一定会被擦除)
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)用 ? 表示,主要有三种形式:
- 无界通配符:
? - 表示任何类型
- 上界通配符:
? extends T - 表示 T 或 T 的子类型
- 下界通配符:
? 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 原则
PECS 是 Producer Extends, Consumer Super 的缩写,是使用通配符的核心原则:
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 类型擦除示例代码
- 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 运行时类型相同,因为泛型类型被擦除了!运行时它们都是:
类型一样,所以 == 结果是 true。
- 类型擦除证明
1
2
3
|
// 通过反射添加不同类型的元素
Method addMethod = stringList.getClass().getMethod("add", Object.class);
addMethod.invoke(stringList, 123); // 运行时成功
|
明明定义的是
按理说只能加字符串!
但运行时它变成了
也就是 List< Object >
所以,可以往 String 列表里加数字 123!,编译器拦得住,但运行时拦不住! 这就是类型擦除最铁的证据!
5.2 类型边界
类型边界 用于限制泛型类型参数的范围,使用 extends 和 super 关键字。
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 方法,才能进行大小比较。
这段代码如何体现“类型边界”?
- 限制泛型必须具备比较能力
通过
<T extends Comparable<T>> 强制要求:
- 传入的类型必须实现
Comparable
- 只有这样才能调用
compareTo 进行大小比较
如果不加这个边界,编译器会报错,因为它无法确定 T 是否有比较方法。
- 保证方法内部逻辑合法
代码中用到:
1
|
element.compareTo(max);
|
compareTo 是 Comparable 接口中的方法
- 类型边界保证了该方法一定存在
- 编译器可以安全放行,不会出现类型不安全调用
- 约束参数与返回值类型
1
|
public static <T extends Comparable<T>> T findMax(List<T> list)
|
- 参数
List<T>:列表元素必须是可比较类型
- 返回值
T:返回的最大值也必须是同一可比较类型
- 整个方法的输入、输出、逻辑共用同一个受约束的泛型 T
- 实际使用时自动类型匹配
在
main 中:
1
2
3
4
5
|
List<Integer> numbers = ...;
List<String> words = ...;
findMax(numbers);
findMax(words);
|
Integer 实现了 Comparable<Integer>
String 实现了 Comparable<String>
- 都满足边界约束,因此可以正常调用
类型边界的作用总结
- 对泛型类型做能力限制,只允许符合条件的类型传入
- 让编译器提前校验方法调用的合法性(如
compareTo)
- 既保持泛型通用性,又保证类型安全
- 是泛型方法实现“通用算法”(如求最大、排序)的核心手段
六、泛型的高级应用
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 泛型与序列化
泛型在序列化是要注意以下内容:
- 泛型类要实现序列化。如 Box 实现了 Serializable。序列化时,Box< String > 会被类型擦除为 Box(原始类型),JVM 不会记录泛型 < String > 的信息,只会序列化 content 的实际值(“Hello, Generics!")和 Box 类的结构;
- 泛型类实现 Serializable,且泛型对应的实际类型(T的具体类型)也要实现 Serializable。示例中 T 是 String(String 实现了 Serializable),所以能正常序列化;
- 反序列化时,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 );
}
}
|
桥接方法的作用:
- 满足重写(匹配父类
add(Object))
- 内部强转类型,保证安全
- 对程序员完全隐藏
第三步:隐藏的坑!(代码演示的就是这个)
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();
|
九、总结
- 泛型本质:将类型作为参数,实现代码复用与编译期类型安全。
- 类型擦除:泛型信息仅存在编译期,运行时会被擦除,无法用
instanceof 判断具体泛型类型(如 List<String>)。
- 通配符:使用
?、? extends T、? super T 实现灵活兼容。
- PECS 原则:读取(生产者)用
extends,写入(消费者)用 super。
- 类型边界:通过
<T extends 类/接口> 限制泛型范围,增强类型检查。
- 禁止使用原生类型(如
List),必须声明具体泛型(如 List<String>),否则失去类型检查。
- 泛型类如需序列化,要实现
Serializable,内部字段也需可序列化。
- 反序列化泛型对象优先使用
? 通配符,避免强制转换;反序列化后需判断类型,防止 ClassCastException。
- 泛型声明尽量精简,只在需要通用、类型安全的场景使用。
- 使用有意义的类型参数名,合理利用
<> 钻石运算符简化代码。
泛型是 Java 中非常强大的特性,掌握好泛型可以写出更加灵活、安全、可维护的代码。通过本文的学习,相信你已经对泛型有了更深入的理解,可以在实际开发中灵活运用泛型来解决问题。