泛型基础与类型安全 | JavaSE
泛型基础与类型安全
一、学习目标
完成本章后,你应该能够:
- 能够解释什么是泛型(Generics)。
- 能够理解“把类型作为参数”这一泛型核心思想。
- 能够说明泛型为什么能够把大量类型错误提前到编译阶段发现。
- 能够使用
ArrayList<String>、ArrayList<Integer>等参数化类型。 - 能够解释为什么使用泛型后取出元素通常不再需要手动强制类型转换。
- 能够理解原始类型(Raw Type)带来的类型安全风险。
- 能够解释为什么泛型不能直接使用
int等基本类型作为类型实参。 - 能够初步理解泛型的类型擦除(Type Erasure)。
- 能够区分“泛型参数”与“真正的数据参数”。
- 为后续泛型类、泛型接口、泛型方法和通配符建立知识基础。
二、核心知识
2.1 什么是泛型
泛型(Generics)的核心思想可以概括为:
把“类型”也作为一种参数。
普通方法的参数可能是:
print("Java");
print("Python");
这里变化的是:
数据
而泛型关注:
类型
例如:
ArrayList<String>
这里:
String
就是传递给:
ArrayList
的类型参数。
再例如:
ArrayList<Integer>
这里传入的是:
Integer
因此可以先把泛型理解成:
ArrayList<类型>
2.2 没有泛型会发生什么
假设没有类型约束:
ArrayList list = new ArrayList();
list.add("Java");
list.add("Spring");
list.add(100);
这个集合里面出现:
String
String
Integer
混合数据。
取出来时:
Object value = list.get(0);
得到的是:
Object
如果程序员认为所有元素都是 String:
String text =
(String) list.get(2);
但:
list.get(2)
实际上是:
Integer
运行时就可能出现:
ClassCastException
也就是说:
错误数据已经放进集合
↓
编译器没有及时阻止
↓
程序运行一段时间
↓
取数据并强转
↓
ClassCastException
错误发现得太晚。
2.3 泛型怎样解决问题
加入泛型:
ArrayList<String> list =
new ArrayList<String>();
list.add("Java");
list.add("Spring");
// list.add(100);
现在:
ArrayList<String>
明确规定:
集合元素类型 = String
于是:
list.add(100);
会在:
编译阶段
直接被拒绝。
错误从:
运行时
提前到了:
编译时
这就是泛型最重要的价值之一:
提高类型安全,把大量类型错误尽可能提前到编译阶段发现。
2.4 泛型的第一个核心价值:类型安全
没有泛型:
什么对象都可能放进去
↓
运行时才发现类型错了
使用泛型:
提前声明允许的数据类型
↓
编译器检查
↓
错误数据不能合法加入
例如:
ArrayList<Integer> scores =
new ArrayList<>();
scores.add(90);
scores.add(100);
// scores.add("Java");
最后一行:
类型不符合 Integer
编译器直接拒绝。
2.5 第二个核心价值:减少强制类型转换
没有泛型时:
ArrayList list = new ArrayList();
list.add("Java");
Object value = list.get(0);
String text = (String) value;
需要手动:
(String)
而使用泛型:
ArrayList<String> list =
new ArrayList<>();
list.add("Java");
String text = list.get(0);
因为编译器已经知道:
list 中的元素类型是 String
所以:
get()
在源码类型系统中就能够被视为返回:
String
不需要开发者自己写强制转换。
2.6 参数化类型
下面:
ArrayList<String>
称为一个:
参数化类型(Parameterized Type)
可以理解为:
泛型类
+
具体类型实参
类似:
ArrayList<String>
ArrayList<Integer>
ArrayList<Student>
ArrayList<Teacher>
同一个:
ArrayList
可以根据需要约束不同数据类型。
2.7 类型参数和类型实参
以后定义泛型时会看到:
T
E
K
V
这些通常叫:
类型参数(Type Parameter)
而真正使用:
ArrayList<String>
时:
String
是具体类型实参。
可以类比普通方法:
public static void print(String text)
这里:
text
是形参。
调用:
print("Java");
这里:
"Java"
是实际数据。
而泛型则是在:
类型层面
做类似参数化。
2.8 常见泛型类型字母
后续会经常看到:
T
E
K
V
它们只是标识符,但社区形成了稳定命名习惯:
| 名称 | 常见含义 |
| ---- | -------- |
| T | Type |
| E | Element |
| K | Key |
| V | Value |
例如以后:
Map<K, V>
可以理解成:
K = Key 类型
V = Value 类型
而:
List<E>
中的:
E
通常表示:
Element
2.9 泛型不能直接使用基本类型
错误:
// ArrayList<int> list =
// new ArrayList<>();
正确:
ArrayList<Integer> list =
new ArrayList<>();
类似:
int → Integer
double → Double
char → Character
boolean → Boolean
这正是上一章包装类和本章泛型之间的知识连接。
三、使用方法
3.1 String 集合
import java.util.ArrayList;
public class GenericDemo {
public static void main(String[] args) {
ArrayList<String> languages =
new ArrayList<>();
languages.add("Java");
languages.add("Python");
languages.add("C++");
String language =
languages.get(0);
System.out.println(language);
}
}
这里泛型保证:
add()
只能按照 String 类型要求添加
get()
源码层面直接获得 String
3.2 Integer 集合
ArrayList<Integer> scores =
new ArrayList<>();
scores.add(95);
scores.add(100);
scores.add(88);
int score = scores.get(0);
这里同时出现两套机制:
泛型
+
包装类自动装箱/拆箱
完整过程:
scores.add(95)
int 95
↓ 自动装箱
Integer
↓
符合 ArrayList<Integer>
取出:
scores.get(0)
↓
Integer
↓ 自动拆箱
int score
3.3 Java 7+ 的菱形操作符
早期可以写:
ArrayList<String> list =
new ArrayList<String>();
现代 Java 通常写:
ArrayList<String> list =
new ArrayList<>();
右边:
<>
称为:
Diamond Operator
菱形操作符
编译器可以根据上下文推断:
这里的类型实参也是 String
所以不需要重复:
String
3.4 泛型并不是限制集合只能创建一种版本
下面都合法:
ArrayList<String> names =
new ArrayList<>();
ArrayList<Integer> ages =
new ArrayList<>();
ArrayList<Double> prices =
new ArrayList<>();
ArrayList<Student> students =
new ArrayList<>();
泛型的意义正是:
同一套数据结构代码
+
不同具体类型
而不是为每种类型重新开发:
StringArrayList
IntegerArrayList
StudentArrayList
3.5 泛型类型必须保持一致
例如:
ArrayList<String> list =
new ArrayList<>();
那么:
list.add("Java");
合法。
但:
// list.add(100);
不合法。
同样:
String value = list.get(0);
合法。
四、原理与进阶
4.1 泛型把错误提前到编译阶段
这是泛型必须真正理解的地方。
没有泛型时:
加入错误类型
↓
编译可能通过
↓
运行
↓
强转
↓
ClassCastException
泛型:
加入错误类型
↓
javac 进行类型检查
↓
直接编译失败
因此泛型实际上体现了一个非常重要的软件工程思想:
能在编译阶段发现的问题,就尽量不要拖到运行阶段。
4.2 泛型提供的是静态类型安全
泛型最主要的检查发生在:
编译阶段
所以它属于:
静态类型系统
的一部分。
例如:
ArrayList<String> list =
new ArrayList<>();
// list.add(100);
并不是 JVM 运行到:
add(100)
才阻止。
而是:
javac
已经知道:
Integer
不能作为 String 元素加入
4.3 什么是类型擦除
Java 泛型的实现涉及:
类型擦除(Type Erasure)
简单理解:
Java 编译器在编译泛型代码时执行类型检查,并将泛型类型映射为对应的擦除类型。
例如源码层面:
ArrayList<String>
编译后的运行时表示并不是创建了一个全新的:
ArrayListString
类型。
同样:
ArrayList<Integer>
也不会为 Integer 再产生一个全新 ArrayList 类。
4.4 “泛型全部擦成 Object”并不准确
初学时经常会听到:
泛型编译后都会变成 Object。
这是为了帮助理解,但并不严谨。
准确地说:
参数化类型
例如 ArrayList<String>
↓
擦除为其原始类型 ArrayList
而类型变量:
T
如果没有边界:
擦除通常是 Object
但如果以后定义:
T extends Number
那么 T 的擦除会与其边界有关,而不是简单全部变成:
Object
因此本阶段只需要记住:
Java 泛型主要通过编译期类型检查 + 类型擦除实现,但“所有泛型最终都直接变成 Object”只是过度简化的说法。
类型擦除更深入的机制以后可以继续扩展。
4.5 编译器还会帮助插入必要的类型转换
例如源码:
ArrayList<String> list =
new ArrayList<>();
String value = list.get(0);
开发者没有写:
(String)
但是类型擦除后,编译器仍然需要维护源码层面的类型安全语义。
因此编译器会完成必要的转换工作。
这就是为什么:
使用泛型
可以让我们的代码同时获得:
更安全
+
更简洁
4.6 原始类型 Raw Type
下面:
ArrayList list =
new ArrayList();
没有写:
<...>
称为使用:
Raw Type
原始类型
它主要是 Java 泛型引入后为了兼容旧代码而保留的机制。
现代业务代码不应该无理由这样写。
因为:
ArrayList list =
new ArrayList();
list.add("Java");
list.add(100);
list.add(true);
类型约束基本丢失。
IDE / 编译器还可能给出:
unchecked warning
所以应该优先:
ArrayList<String> list =
new ArrayList<>();
4.7 泛型不是运行时万能安全网
即使泛型提供强大的编译期检查,
如果开发者:
使用 Raw Type
进行 unchecked cast
故意绕开类型系统
仍然可能破坏类型安全。
例如:
ArrayList<String> strings =
new ArrayList<>();
ArrayList raw = strings;
raw.add(100);
这种代码可能产生:
unchecked warning
并把本不应该出现的 Integer 混入集合。
以后取值时仍可能发生:
ClassCastException
所以:
编译器已经给出 unchecked warning 时,不应该习惯性无视。
五、实践应用
5.1 学生集合
ArrayList<Student> students =
new ArrayList<>();
现在整个集合在源码层面都围绕:
Student
工作。
这样比:
ArrayList list
更加安全、清晰。
5.2 Service 返回值
以后可以看到:
List<User>
List<Order>
List<Product>
泛型直接告诉开发者:
这个集合保存的是什么业务对象
因此泛型不仅提供类型安全,
还提升了:
API 可读性
5.3 Map 的 K 与 V
以后集合框架会学习:
Map<String, User>
从类型上就能够读出:
Key → String
Value → User
这就是:
K
V
这种类型参数设计的价值。
六、常见问题
6.1 泛型是不是只能用于集合?
不是。
集合只是泛型最典型的使用场景。
后面还会学习:
泛型类
泛型接口
泛型方法
6.2 ArrayList 和 ArrayList 是两个完全不同的 class 文件吗?
不是简单为每一种参数类型重新生成一个独立 ArrayList 实现类。
Java 泛型使用类型擦除机制实现。
6.3 泛型为什么能减少强制类型转换?
因为编译器已经知道:
集合中的元素类型
例如:
ArrayList<String>
取值时源码类型就是:
String
不需要程序员自己:
(String)
6.4 泛型为什么不能用 int?
因为泛型类型实参使用引用类型。
所以:
int
需要对应:
Integer
包装类。
6.5 什么是 Raw Type?
例如:
ArrayList list =
new ArrayList();
没有提供泛型类型实参。
它主要用于历史兼容。
现代代码应该避免无理由使用。
6.6 unchecked warning 可以无视吗?
不能形成无视习惯。
它通常意味着:
编译器已经无法完整证明当前类型操作安全
需要认真判断:
为什么发生 unchecked?
是否使用了 Raw Type?
是否发生了不安全强制转换?
6.7 泛型是否意味着绝对不会出现 ClassCastException?
不是。
泛型能够大幅减少相关错误,
但是如果:
绕开泛型
使用原始类型
执行不安全转换
依然可能污染集合并造成运行时异常。
6.8 T、E、K、V 是 Java 关键字吗?
不是。
它们只是约定俗成的类型参数名称。
理论上也可以定义其他合法标识符。
但是遵守社区惯例通常更容易阅读代码。
6.9 为什么说泛型是“类型参数化”?
普通参数:
方法接收数据
泛型则允许:
类 / 接口 / 方法
接收类型信息
因此:
类型
本身成为设计的一部分。
七、练习与验收
7.1 知识问答
- 什么是泛型?
- 为什么可以把泛型理解成“把类型作为参数”?
- 泛型主要解决什么问题?
- 没有泛型时集合混入不同类型会造成什么风险?
- 泛型为什么可以提高类型安全?
- 泛型为什么可以减少强制类型转换?
ArrayList<String>中String表示什么?- 什么叫参数化类型?
T、E、K、V通常表示什么?- 为什么泛型不能直接写
int? ArrayList<Integer>与自动装箱有什么联系?- 什么是类型擦除?
- 为什么“泛型全部擦成 Object”不是严谨描述?
- 什么是 Raw Type?
- Raw Type 为什么会削弱类型安全?
- unchecked warning 为什么值得重视?
- 泛型能否百分百消除 ClassCastException?
7.2 代码阅读
阅读:
import java.util.ArrayList;
public class GenericRead01 {
public static void main(String[] args) {
ArrayList<String> list =
new ArrayList<>();
list.add("Java");
list.add("Spring");
// list.add(100);
String value = list.get(0);
System.out.println(value);
}
}
回答:
- 为什么
list.add(100)不能通过编译? - 编译器从哪里知道集合只能保存 String?
- 为什么
get(0)不需要(String)? - 这体现泛型哪两个核心价值?
阅读:
import java.util.ArrayList;
public class GenericRead02 {
public static void main(String[] args) {
ArrayList list =
new ArrayList();
list.add("Java");
list.add(100);
String value =
(String) list.get(1);
System.out.println(value);
}
}
回答:
- 为什么集合允许加入
"Java"和100? - 这里使用了什么类型?
- 程序可能在哪个阶段出问题?
- 可能出现什么异常?
- 使用泛型应该怎样重写?
7.3 手写代码
任务一:字符串集合
创建:
ArrayList<String>
保存:
Java
C++
Python
Go
要求:
- 遍历输出。
- 不允许任何强制类型转换。
- 尝试加入整数并观察编译器反馈。
任务二:整数集合
创建:
ArrayList<Integer>
保存多个整数。
要求:
- 计算总和。
- 指出添加时发生的装箱。
- 指出计算时发生的拆箱。
任务三:学生集合
自定义:
Student
然后创建:
ArrayList<Student>
完成:
添加学生
获取学生
输出姓名
整个过程不允许使用强制类型转换。
7.4 Debug
下面代码存在类型设计问题:
import java.util.ArrayList;
public class GenericDebug {
public static void main(String[] args) {
ArrayList list =
new ArrayList();
list.add("Java");
list.add(100);
list.add(true);
for (Object value : list) {
String text =
(String) value;
System.out.println(
text.toUpperCase()
);
}
}
}
要求:
- 为什么代码能够加入三种不同类型?
- 第几次遍历可能出现问题?
- 可能出现什么异常?
- 为什么这是“类型错误发现过晚”的例子?
- 使用泛型重构。
- 让错误在编译阶段直接暴露。
继续分析:
ArrayList<String> strings =
new ArrayList<>();
ArrayList raw = strings;
raw.add(100);
回答:
- 为什么
raw.add(100)仍可能通过编译? - 编译器可能给什么警告?
- 为什么这种行为破坏了泛型安全?
- 后续
strings.get()为什么可能出现运行时问题?
7.5 综合训练
设计一个“课程成绩管理”数据模型。
要求存在:
学生姓名集合
课程名称集合
成绩集合
分别使用合理泛型:
ArrayList<String>
ArrayList<Integer>
完成:
- 添加数据。
- 获取数据。
- 遍历数据。
- 计算平均成绩。
- 故意尝试向成绩集合加入字符串。
- 观察编译阶段反馈。
- 再使用 Raw Type 绕开约束做一次实验。
- 比较两种方案错误出现的阶段。
- 总结泛型为什么提高程序安全性。
7.6 本章验收
- [ ] 能闭卷解释什么是泛型。
- [ ] 能解释“类型参数化”。
- [ ] 能解释泛型怎样把类型错误提前到编译期。
- [ ] 能独立使用
ArrayList<String>。 - [ ] 能独立使用
ArrayList<Integer>。 - [ ] 能解释泛型为什么减少强转。
- [ ] 能解释泛型为什么不能使用基本类型。
- [ ] 能说明包装类与泛型之间的联系。
- [ ] 能解释 Raw Type 的风险。
- [ ] 能初步解释类型擦除。
- [ ] 不再使用“所有泛型最后都简单变成 Object”这种绝对表述。