泛型
# 14.泛型
# 目录介绍
# 14.1 泛型入门
# 14.1.1 为什么需要泛型
没有泛型时,集合只能存 Object,取出时需要强制转换,容易出错:
// 没有泛型
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // 可以放任何类型
String s = (String) list.get(1); // 运行时 ClassCastException!
// 使用泛型
ArrayList<String> list2 = new ArrayList<>();
list2.add("Hello");
// list2.add(123); // 编译错误!类型安全
String s2 = list2.get(0); // 不需要强制转换
2
3
4
5
6
7
8
9
10
11
# 14.1.2 泛型的好处
- 类型安全:编译期检查类型,避免 ClassCastException。
- 消除强制转换:不需要手动类型转换。
- 代码复用:一份代码支持多种类型。
对比 C++:C++ 的模板(template)在编译时展开,为每种类型生成独立的代码。Java 的泛型通过类型擦除实现,运行时没有泛型信息。
疑惑:泛型只是用来避免强制转换吗?它的设计背景是什么?
JDK 1.4 及之前,集合框架只能存储
Object,开发者每次取出元素都要手动强制转换,这不仅繁琐而且极易出错。ClassCastException 成为 Java 最常见的运行时异常之一。
答疑:泛型的核心目标是将类型检查从运行时前移到编译时。这是一种"快速失败"(fail-fast)的设计理念——错误越早暴露,修复成本越低。编译期的类型检查比运行时的 ClassCastException 要好得多。
论证:Martin Odersky(后来创建了 Scala)在 1998 年提出了 GJ(Generic Java)提案,后来发展成 JSR 14,最终在 JDK 5(2004年)正式引入泛型。设计时面临一个关键抉择:是否向后兼容。C# 选择了不兼容(.NET 2.0 的泛型是 CLR 层面真正支持的),而 Java 选择了向后兼容,采用类型擦除方案——编译后的字节码与 JDK 1.4 完全兼容。
结果展示:这个向后兼容的决定深刻影响了 Java 泛型的设计。它带来了好处(旧代码无需修改就能运行),也带来了限制(不能创建泛型数组、不能用 instanceof 检查泛型类型等)。理解这个历史背景,才能真正理解泛型的种种"奇怪"限制。
# 14.1.3 综合案例:类型安全对比
本案例对比无泛型和有泛型的代码,直观展示泛型的好处。
import java.util.*;
/**
* 类型安全对比 —— 泛型入门综合案例
* 知识点:原始类型的问题、泛型的类型安全和代码简洁
*/
public class GenericIntro {
public static void main(String[] args) {
System.out.println("===== 类型安全对比 =====\n");
// 1. 无泛型(原始类型):类型不安全
System.out.println("--- 无泛型(危险)---");
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(123); // 可以混入任何类型!
// String s = (String) rawList.get(1); // 运行时 ClassCastException!
System.out.println(" rawList: " + rawList + " (混合类型,取值需强转)");
// 2. 有泛型:编译期类型安全
System.out.println("\n--- 有泛型(安全)---");
List<String> safeList = new ArrayList<>();
safeList.add("hello");
safeList.add("world");
// safeList.add(123); // 编译错误!类型不匹配
String s = safeList.get(0); // 无需强转
System.out.println(" safeList: " + safeList + " (类型统一,无需强转)");
// 3. 泛型带来的好处总结
System.out.println("\n--- 泛型的好处 ---");
System.out.println(" 1. 编译期类型检查,避免 ClassCastException");
System.out.println(" 2. 自动类型转换,无需手动强转");
System.out.println(" 3. 代码复用:一套代码适用于多种类型");
}
}
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
# 14.1.4 泛型入门训练题
训练1:创建一个不使用泛型的 ArrayList,先添加一个 String,再添加一个 Integer,然后尝试将第二个元素取出并转为 String。观察运行时会发生什么错误。再用泛型 ArrayList<String> 重写,观察编译时的区别。
训练2:Java 的泛型和 C++ 的模板都能实现"一份代码支持多种类型",但实现方式完全不同。请简要描述两者的区别,并说明 Java 为什么选择了类型擦除方案。
思考:如果 Java 泛型不使用类型擦除,而是像 C# 那样在虚拟机层面真正支持泛型(称为"具化泛型",reified generics),Java 的集合框架会有哪些不同?
# 14.2 泛型类
# 14.2.1 泛型类定义
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(123);
int num = intBox.get();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 14.2.2 多类型参数
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; }
}
Pair<String, Integer> pair = new Pair<>("age", 25);
System.out.println(pair.getKey() + ": " + pair.getValue());
2
3
4
5
6
7
8
9
10
11
12
13
14
15
常见的类型参数命名规范:
| 参数名 | 含义 |
|---|---|
T | Type(类型) |
E | Element(元素,常用于集合) |
K | Key(键) |
V | Value(值) |
N | Number(数字) |
R | Result(返回值) |
S, U | 第二、第三类型参数 |
泛型类的继承:
// 泛型类可以继承
public class NamedBox<T> extends Box<T> {
private String name;
public NamedBox(String name) {
this.name = name;
}
@Override
public String toString() {
return name + ": " + get();
}
}
// 继承时可以指定具体类型
public class StringBox extends Box<String> {
// 此时 StringBox 不再是泛型类
// put(String item) 和 String get() 已经确定了类型
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 14.2.3 综合案例:通用键值对容器
本案例实现一个泛型类,演示多类型参数和泛型嵌套。
import java.util.*;
/**
* 通用键值对容器 —— 泛型类综合案例
* 知识点:泛型类定义、多类型参数<K,V>、泛型嵌套
*/
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; }
@Override
public String toString() {
return key + " = " + value;
}
}
class PairList<K, V> {
private List<Pair<K, V>> pairs = new ArrayList<>(); // 泛型嵌套
public void add(K key, V value) {
pairs.add(new Pair<>(key, value));
}
public V findByKey(K key) {
for (Pair<K, V> p : pairs) {
if (p.getKey().equals(key)) return p.getValue();
}
return null;
}
public void printAll() {
for (Pair<K, V> p : pairs) {
System.out.println(" " + p);
}
}
}
public class GenericClassDemo {
public static void main(String[] args) {
System.out.println("===== 通用键值对容器 =====\n");
// 字符串→整数
PairList<String, Integer> scores = new PairList<>();
scores.add("张三", 92);
scores.add("李四", 85);
scores.add("王五", 78);
System.out.println("--- 成绩表 ---");
scores.printAll();
System.out.println(" 查找张三: " + scores.findByKey("张三"));
// 整数→字符串
PairList<Integer, String> errorCodes = new PairList<>();
errorCodes.add(200, "OK");
errorCodes.add(404, "Not Found");
errorCodes.add(500, "Server Error");
System.out.println("\n--- 错误码 ---");
errorCodes.printAll();
System.out.println(" 查找404: " + errorCodes.findByKey(404));
}
}
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
63
64
65
66
67
68
# 14.2.4 泛型类训练题
训练1:实现一个泛型类 Triple<A, B, C>,存储三个不同类型的值,并提供 getFirst()、getSecond()、getThird() 方法。
训练2:实现一个泛型类 Range<T extends Comparable<T>>,表示一个范围 [min, max],提供 contains(T value) 方法判断值是否在范围内。
思考:Box<String> 是 Box<Object> 的子类吗?为什么?这和数组 String[] 是 Object[] 的子类的行为有什么不同?
# 14.3 泛型方法
# 14.3.1 泛型方法定义
public class ArrayUtils {
// 泛型方法:<T> 放在返回类型前面
public static <T> void printArray(T[] arr) {
for (T item : arr) {
System.out.print(item + " ");
}
System.out.println();
}
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 14.3.2 泛型方法使用
Integer[] nums = {1, 2, 3, 4, 5};
String[] names = {"Alice", "Bob", "Charlie"};
ArrayUtils.printArray(nums); // 1 2 3 4 5
ArrayUtils.printArray(names); // Alice Bob Charlie
int maxNum = ArrayUtils.max(10, 20); // 20
String maxStr = ArrayUtils.max("abc", "xyz"); // xyz
2
3
4
5
6
7
8
泛型方法 vs 泛型类的方法:
public class Demo<T> {
// 这是泛型类的方法,T 来自类的类型参数
public T getFirst(List<T> list) {
return list.get(0);
}
// 这是泛型方法,<E> 是方法自己声明的类型参数,与类的 T 无关
public <E> E getFirst2(List<E> list) {
return list.get(0);
}
// 静态方法不能使用类的类型参数 T(因为 T 需要实例化才确定)
// public static T wrong(T item) { } // 编译错误!
// 静态方法必须声明自己的类型参数
public static <E> E correct(E item) { return item; } // OK
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 14.3.3 综合案例:泛型工具方法集
本案例实现多个泛型方法,演示类型推断和有界类型参数。
import java.util.*;
/**
* 泛型工具方法集 —— 泛型方法综合案例
* 知识点:泛型方法定义、类型推断、有界类型参数<T extends Comparable>
*/
public class GenericUtils {
// 泛型方法:交换数组元素
static <T> void swap(T[] arr, int i, int j) {
T temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// 有界类型参数:找最大值
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// 泛型方法:列表转数组
static <T> Object[] toArray(List<T> list) {
Object[] arr = new Object[list.size()];
for (int i = 0; i < list.size(); i++) arr[i] = list.get(i);
return arr;
}
// 泛型方法:打印任意数组
static <T> void printArray(T[] arr) {
System.out.print(" [");
for (int i = 0; i < arr.length; i++) {
if (i > 0) System.out.print(", ");
System.out.print(arr[i]);
}
System.out.println("]");
}
public static void main(String[] args) {
System.out.println("===== 泛型工具方法集 =====\n");
// 1. swap
Integer[] nums = {1, 2, 3, 4, 5};
System.out.println("--- swap ---");
System.out.print(" 交换前: "); printArray(nums);
swap(nums, 0, 4); // 类型自动推断为 Integer
System.out.print(" 交换后: "); printArray(nums);
// 2. max(有界类型参数)
System.out.println("\n--- max ---");
System.out.println(" max(3, 7) = " + max(3, 7));
System.out.println(" max(\"abc\", \"xyz\") = " + max("abc", "xyz"));
// 3. printArray 对不同类型
System.out.println("\n--- printArray ---");
String[] strs = {"Java", "Python", "Go"};
printArray(strs);
Double[] dbls = {3.14, 2.71, 1.41};
printArray(dbls);
}
}
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
# 14.3.4 泛型方法训练题
训练1:实现一个泛型方法 <T> int countOccurrences(T[] array, T target),统计数组中某个元素出现的次数。
训练2:实现一个泛型方法 <T extends Comparable<T>> T[] sort(T[] array),对数组进行排序并返回。
思考:泛型方法中的 <T> 是在什么时候确定具体类型的?是编译时还是运行时?如果 ArrayUtils.max(10, 20) 没有显式指定类型参数,编译器是如何推断出 T 是 Integer 的?
# 14.4 泛型接口
# 14.4.1 泛型接口定义
public interface Repository<T> {
void save(T entity);
T findById(int id);
List<T> findAll();
}
public class AccountRepository implements Repository<Account> {
@Override
public void save(Account entity) { /* 实现 */ }
@Override
public Account findById(int id) { /* 实现 */ return null; }
@Override
public List<Account> findAll() { /* 实现 */ return null; }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
泛型接口实现的两种方式:
// 方式一:实现时指定具体类型
public class StringComparator implements Comparator<String> {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
}
// 方式二:实现时保留泛型(实现类也是泛型类)
public class GenericRepository<T> implements Repository<T> {
private List<T> storage = new ArrayList<>();
@Override
public void save(T entity) {
storage.add(entity);
}
@Override
public T findById(int id) {
return id < storage.size() ? storage.get(id) : null;
}
@Override
public List<T> findAll() {
return Collections.unmodifiableList(storage);
}
}
// 使用
Repository<String> strRepo = new GenericRepository<>();
strRepo.save("Hello");
Repository<Integer> intRepo = new GenericRepository<>();
intRepo.save(42);
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
# 14.4.2 综合案例:泛型接口实现
本案例通过不同方式实现泛型接口,演示指定类型和保留泛型参数。
import java.util.*;
/**
* 泛型接口实现 —— 泛型接口综合案例
* 知识点:泛型接口定义、实现时指定类型、实现时保留泛型
*/
interface Repository<T> {
void save(T entity);
T findById(int id);
List<T> findAll();
}
// 实现时指定具体类型
class StringRepository implements Repository<String> {
private Map<Integer, String> store = new HashMap<>();
private int nextId = 1;
@Override
public void save(String entity) {
store.put(nextId++, entity);
}
@Override
public String findById(int id) {
return store.get(id);
}
@Override
public List<String> findAll() {
return new ArrayList<>(store.values());
}
}
// 实现时保留泛型参数(更通用)
class MemoryRepository<T> implements Repository<T> {
private Map<Integer, T> store = new HashMap<>();
private int nextId = 1;
@Override
public void save(T entity) {
store.put(nextId++, entity);
}
@Override
public T findById(int id) {
return store.get(id);
}
@Override
public List<T> findAll() {
return new ArrayList<>(store.values());
}
}
public class GenericInterfaceDemo {
public static void main(String[] args) {
System.out.println("===== 泛型接口实现 =====\n");
// 指定类型的实现
StringRepository strRepo = new StringRepository();
strRepo.save("Hello");
strRepo.save("World");
System.out.println("StringRepository: " + strRepo.findAll());
// 保留泛型的实现(可用于任何类型)
MemoryRepository<Integer> intRepo = new MemoryRepository<>();
intRepo.save(100);
intRepo.save(200);
System.out.println("MemoryRepository<Integer>: " + intRepo.findAll());
MemoryRepository<Double> dblRepo = new MemoryRepository<>();
dblRepo.save(3.14);
System.out.println("MemoryRepository<Double>: " + dblRepo.findAll());
}
}
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
63
64
65
66
67
68
69
70
71
72
73
74
75
# 14.4.3 泛型接口训练题
训练1:定义一个泛型接口 Converter<F, T>,包含方法 T convert(F from)。然后实现 StringToIntConverter(将 String 转 Integer)和 IntToStringConverter(将 Integer 转 String)。
训练2:定义一个泛型接口 Validator<T>,包含方法 boolean validate(T item) 和 String getErrorMessage()。实现 AgeValidator(验证年龄在0-150之间)和 EmailValidator(验证邮箱包含@符号)。
思考:Java 的 Comparable<T> 和 Comparator<T> 都是泛型接口,为什么设计成泛型的而不是直接用 Object?
# 14.5 通配符
# 14.5.1 无界通配符
// ? 表示任意类型
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
printList(List.of(1, 2, 3));
printList(List.of("a", "b", "c"));
2
3
4
5
6
7
8
9
# 14.5.2 上界通配符
// ? extends Number:Number 或 Number 的子类
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number num : list) {
total += num.doubleValue();
}
return total;
}
sum(List.of(1, 2, 3)); // List<Integer>,OK
sum(List.of(1.5, 2.5)); // List<Double>,OK
// sum(List.of("a", "b")); // List<String>,编译错误
2
3
4
5
6
7
8
9
10
11
12
# 14.5.3 下界通配符
// ? super Integer:Integer 或 Integer 的父类
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> numList = new ArrayList<>();
addNumbers(numList); // OK
List<Object> objList = new ArrayList<>();
addNumbers(objList); // OK
2
3
4
5
6
7
8
9
10
11
PECS 原则:Producer Extends, Consumer Super。读取数据用
extends,写入数据用super。
# 14.5.4 PECS原则深度解析
疑惑:为什么 List<? extends Number> 不能添加元素,而 List<? super Integer> 不能安全读取?
这是泛型通配符最让人困惑的地方。看起来
? extends Number表示"Number 的子类",那为什么不能往里面添加Integer(它就是 Number 的子类啊)?
答疑:关键在于编译器不知道 ? 具体是什么类型。List<? extends Number> 可能是 List<Integer>、List<Double> 或 List<BigDecimal>。如果实际类型是 List<Double>,你往里面添加 Integer 就破坏了类型安全。编译器无法在编译时确定具体类型,所以干脆禁止所有写入操作(除了 null)。
论证:
// 假设允许写入,会发生什么?
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
List<? extends Number> numList = intList; // 合法的向上转型
// numList.add(3.14); // 如果允许,实际添加到了 List<Integer> 中!
// Integer i = intList.get(2); // 取出 3.14 赋给 Integer,类型不安全!
// 但读取是安全的:
Number n = numList.get(0); // OK,取出的一定是 Number 的子类
2
3
4
5
6
7
8
9
10
11
// 反过来,List<? super Integer> 的情况
List<Number> numList2 = new ArrayList<>();
List<? super Integer> superList = numList2;
superList.add(1); // OK,Integer 一定可以放入 Integer 的父类容器
superList.add(2); // OK
// 但读取时不知道具体类型:
// Integer i = superList.get(0); // 编译错误!可能是 Number 或 Object
Object o = superList.get(0); // 只能用 Object 接收
2
3
4
5
6
7
8
9
10
结果展示:PECS 原则的口诀总结:
- Producer Extends:当你只需要从集合中读取数据时,使用
? extends T。集合是数据的"生产者"。 - Consumer Super:当你只需要往集合中写入数据时,使用
? super T。集合是数据的"消费者"。 - 既读又写:不用通配符,直接用
T。
JDK 中的经典应用:Collections.copy(List<? super T> dest, List<? extends T> src)——从 src(生产者)读取数据,写入 dest(消费者)。
# 14.5.5 综合案例:通配符使用场景
本案例通过实际场景演示三种通配符和 PECS 原则。
import java.util.*;
/**
* 通配符使用场景 —— 通配符综合案例
* 知识点:?、? extends T、? super T、PECS原则
*/
public class WildcardDemo {
// ? extends Number:只读(Producer)
static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) {
total += n.doubleValue();
}
return total;
}
// ? super Integer:只写(Consumer)
static void fillNumbers(List<? super Integer> list, int count) {
for (int i = 1; i <= count; i++) {
list.add(i); // 可以写入 Integer
}
}
// ?:只做通用操作(不读不写具体类型)
static void printList(List<?> list) {
System.out.print(" [");
for (int i = 0; i < list.size(); i++) {
if (i > 0) System.out.print(", ");
System.out.print(list.get(i));
}
System.out.println("]");
}
public static void main(String[] args) {
System.out.println("===== 通配符使用场景 =====\n");
// 1. ? extends(上界):从集合中读取
System.out.println("--- ? extends Number(Producer/读取)---");
List<Integer> ints = List.of(1, 2, 3);
List<Double> dbls = List.of(1.5, 2.5);
System.out.println(" sum(Integer): " + sum(ints));
System.out.println(" sum(Double) : " + sum(dbls));
// 2. ? super(下界):往集合中写入
System.out.println("\n--- ? super Integer(Consumer/写入)---");
List<Number> numList = new ArrayList<>();
fillNumbers(numList, 3); // List<Number> 可以接受 Integer
System.out.print(" fillNumbers → "); printList(numList);
// 3. ?(无界):通用操作
System.out.println("\n--- ?(无界通配符)---");
printList(ints);
printList(dbls);
printList(List.of("A", "B", "C"));
// 4. PECS 总结
System.out.println("\n--- PECS原则 ---");
System.out.println(" Producer Extends: 读取用 ? extends T");
System.out.println(" Consumer Super : 写入用 ? super T");
System.out.println(" 既读又写 : 不用通配符,用精确类型 T");
}
}
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
63
# 14.5.6 通配符训练题
训练1:编写方法 double sum(List<? extends Number> list),计算列表中所有数字的和。然后分别用 List<Integer>、List<Double>、List<Long> 测试。
训练2:编写方法 void fillList(List<? super Integer> list, int count),向列表中填充 1 到 count 的整数。然后分别用 List<Integer>、List<Number>、List<Object> 测试。
训练3:以下代码哪些行能编译通过?请逐行分析:
List<? extends Number> list1 = new ArrayList<Integer>();
list1.add(1); // ① 能否通过?
Number n = list1.get(0); // ② 能否通过?
List<? super Integer> list2 = new ArrayList<Number>();
list2.add(1); // ③ 能否通过?
Integer i = list2.get(0); // ④ 能否通过?
2
3
4
5
6
7
思考:List<?> 和 List<Object> 有什么区别?List<?> 和原始类型 List 又有什么区别?
# 14.6 类型擦除
# 14.6.1 擦除机制
类型擦除的底层原理:Java 泛型是通过编译器而非 JVM 实现的。编译时,编译器进行类型检查确保类型安全,然后在生成字节码时擦除所有泛型信息:无界类型参数 <T> 替换为 Object,有界类型参数 <T extends Number> 替换为 Number,并在需要的地方自动插入类型转换指令(checkcast)。这种设计的原因是向后兼容——JDK 5 引入泛型时,需要保证泛型代码与 JDK 1.4 及之前的非泛型代码(原始类型 Raw Type)二进制兼容。虽然运行时泛型信息丢失了,但通过 Signature 属性仍可在 .class 文件中保留泛型签名(供反射使用,如 Field.getGenericType())。
Java 泛型在编译后会被擦除,运行时没有泛型信息:
// 编译前
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0);
// 编译后(类型擦除)
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0); // 编译器自动插入强制转换
2
3
4
5
6
7
8
9
# 14.6.2 擦除的影响
// 1. 不能用基本类型作为类型参数
// List<int> list; // 编译错误,必须用 List<Integer>
// 2. 不能创建泛型数组
// T[] arr = new T[10]; // 编译错误
// 3. 运行时泛型类型相同
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true(都是 ArrayList)
2
3
4
5
6
7
8
9
10
对比 C++:C++ 模板不存在类型擦除,每种类型会生成独立的代码(代码膨胀),但运行时性能更高。Java 通过类型擦除避免了代码膨胀,但牺牲了一些运行时类型信息。
# 14.6.3 绕过类型擦除的方式
虽然泛型信息在运行时被擦除了,但 Java 提供了几种方式来获取泛型信息:
方式一:通过 Signature 属性获取(反射)
// 类的字段、方法参数中的泛型信息保存在 .class 文件的 Signature 属性中
public class GenericInfo {
private List<String> names; // 泛型信息保留在字段签名中
public static void main(String[] args) throws Exception {
Field field = GenericInfo.class.getDeclaredField("names");
Type type = field.getGenericType();
if (type instanceof ParameterizedType pt) {
System.out.println("原始类型:" + pt.getRawType()); // List
System.out.println("类型参数:" + pt.getActualTypeArguments()[0]); // String
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
方式二:通过匿名子类获取(TypeToken 技巧)
// Gson 和 Jackson 框架常用的技巧
// 创建匿名子类时,父类的泛型信息会保留在子类的 Signature 中
abstract class TypeRef<T> {
Type getType() {
Type superClass = getClass().getGenericSuperclass();
return ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
}
// 使用
TypeRef<List<String>> ref = new TypeRef<List<String>>() {}; // 匿名子类
System.out.println(ref.getType()); // java.util.List<java.lang.String>
2
3
4
5
6
7
8
9
10
11
12
方式三:传入 Class 对象
// 最直接的方式:显式传入类型信息
public <T> T create(Class<T> type) throws Exception {
return type.getDeclaredConstructor().newInstance();
}
String s = create(String.class); // 传入 Class 对象作为类型标记
2
3
4
5
6
# 14.6.4 综合案例:类型擦除探测器
本案例通过代码实验揭示类型擦除的行为和影响。
import java.util.*;
import java.lang.reflect.*;
/**
* 类型擦除探测器 —— 类型擦除综合案例
* 知识点:擦除机制、运行时无泛型、反射绕过擦除
*/
public class ErasureDetector {
public static void main(String[] args) throws Exception {
System.out.println("===== 类型擦除探测器 =====\n");
// 1. 运行时类型相同
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println("--- 运行时类型比较 ---");
System.out.println(" List<String>.class == List<Integer>.class : "
+ (strList.getClass() == intList.getClass())); // true!
System.out.println(" 实际类型: " + strList.getClass().getName());
// 2. 无法用 instanceof 判断泛型类型
System.out.println("\n--- instanceof 限制 ---");
System.out.println(" strList instanceof List : " + (strList instanceof List));
// strList instanceof List<String> // 编译错误!
System.out.println(" 无法写: strList instanceof List<String>");
// 3. 反射绕过类型擦除
System.out.println("\n--- 反射绕过擦除 ---");
List<Integer> safeList = new ArrayList<>();
safeList.add(1);
// 通过反射往 List<Integer> 里加 String
Method addMethod = safeList.getClass().getMethod("add", Object.class);
addMethod.invoke(safeList, "非法字符串");
System.out.println(" List<Integer> 内容: " + safeList);
System.out.println(" 类型擦除后运行时无类型检查!");
// 4. 擦除后的方法签名
System.out.println("\n--- 擦除后的方法签名 ---");
for (Method m : safeList.getClass().getMethods()) {
if (m.getName().equals("add") && m.getParameterCount() == 1) {
System.out.println(" add 参数类型: " + m.getParameterTypes()[0].getName());
}
}
System.out.println(" 擦除后:add(Object) 而非 add(Integer)");
// 5. 总结
System.out.println("\n--- 类型擦除总结 ---");
System.out.println(" 编译时: 泛型提供类型安全检查");
System.out.println(" 运行时: 泛型信息被擦除,全部变为 Object");
System.out.println(" 影响 : 不能 new T()、不能 instanceof T、不能创建泛型数组");
}
}
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
# 14.6.5 类型擦除训练题
训练1:编写以下代码,观察输出结果:
ArrayList<String> a = new ArrayList<>();
ArrayList<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass());
System.out.println(a.getClass().getName());
2
3
4
然后思考:既然运行时泛型信息被擦除了,为什么 IDE 和编译器还能检查泛型类型?
训练2:尝试通过反射"绕过"泛型约束,往 ArrayList<String> 中添加一个 Integer:
ArrayList<String> list = new ArrayList<>();
// 用反射获取 add 方法并调用...
2
这说明了什么?
思考:Java 的 Project Valhalla 正在推进"具化泛型"(Reified Generics),让泛型信息在运行时保留。如果成功实现,哪些现有的泛型限制会被消除?