Appearance
第18课:泛型
🎯 学习目标
- 理解泛型的概念和作用
- 掌握泛型类、泛型方法、泛型接口
- 理解类型擦除
- 掌握通配符的使用
📖 一、泛型的概念
为什么需要泛型?
问题:没有泛型时,集合只能存储 Object
java
// JDK 5 之前
List list = new ArrayList();
list.add("Hello");
list.add(123);
String str = (String) list.get(0); // 需要强制转换
String str2 = (String) list.get(1); // ❌ 运行时错误:ClassCastException解决:使用泛型
java
// JDK 5+
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(123); // ❌ 编译错误(类型安全)
String str = list.get(0); // 不需要强制转换📖 二、泛型类
1. 定义泛型类
java
// 泛型类
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String str = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(123);
int num = intBox.get();2. 多个类型参数
java
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);
String key = pair.getKey();
Integer value = pair.getValue();📖 三、泛型方法
java
public class GenericMethod {
// 泛型方法
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
// 使用
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C"};
printArray(intArray); // 1 2 3 4 5
printArray(strArray); // A B C
}
}📖 四、泛型接口
java
public interface Comparable<T> {
int compareTo(T o);
}
public class Student implements Comparable<Student> {
String name;
int score;
@Override
public int compareTo(Student other) {
return this.score - other.score;
}
}📖 五、类型限定
1. 上界限定(extends)
java
// T 必须是 Number 或其子类
public class NumberBox<T extends Number> {
private T value;
public void set(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue(); // 可以调用 Number 的方法
}
}
// 使用
NumberBox<Integer> intBox = new NumberBox<>(); // ✅
NumberBox<Double> doubleBox = new NumberBox<>(); // ✅
// NumberBox<String> strBox = new NumberBox<>(); // ❌ 编译错误2. 多个限定
java
// T 必须同时是 Comparable 和 Serializable
public class Box<T extends Comparable<T> & Serializable> {
private T value;
}📖 六、通配符
1. 无界通配符(?)
java
public static void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 可以接受任何类型的 List
printList(new ArrayList<String>());
printList(new ArrayList<Integer>());2. 上界通配符(? extends T)
java
// 只能读取,不能添加(除了 null)
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number num : list) {
sum += num.doubleValue();
}
return sum;
}
// 使用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.0, 2.0, 3.0);
System.out.println(sumOfList(intList)); // 6.0
System.out.println(sumOfList(doubleList)); // 6.03. 下界通配符(? super T)
java
// 只能添加,不能读取(只能读取为 Object)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// 使用
List<Number> numList = new ArrayList<>();
addNumbers(numList);4. PECS 原则
Producer Extends, Consumer Super
java
// 从集合读取数据 → 使用 extends
public static void copy(List<? extends Number> src,
List<? super Number> dest) {
for (Number num : src) {
dest.add(num);
}
}📖 七、类型擦除
1. 什么是类型擦除?
泛型信息只存在于编译期,运行时被擦除。
java
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
// 运行时都是 ArrayList
System.out.println(list1.getClass() == list2.getClass()); // true2. 类型擦除的影响
java
// ❌ 不能创建泛型数组
// T[] array = new T[10]; // 编译错误
// ❌ 不能使用 instanceof
// if (obj instanceof List<String>) { } // 编译错误
// ✅ 可以这样
if (obj instanceof List<?>) { }💡 最佳实践
- 优先使用泛型
java
// ✅ 好
List<String> list = new ArrayList<>();
// ❌ 不好(原始类型)
List list = new ArrayList();- 使用有意义的类型参数名
java
// 常用约定
T - Type(类型)
E - Element(元素)
K - Key(键)
V - Value(值)
N - Number(数字)- PECS 原则
- 生产者用 extends
- 消费者用 super
⚠️ 八、常见陷阱
陷阱1:使用原始类型
java
List list = new ArrayList();
list.add("abc");
list.add(123);原始类型绕过编译期检查,错误会推迟到运行时变成 ClassCastException。
陷阱2:误以为 List<String> 是 List<Object> 的子类型
java
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误如果允许这样赋值,就可以通过 objects.add(123) 把 Integer 放进字符串列表,破坏类型安全。
陷阱3:泛型数组
java
// List<String>[] array = new List<String>[10]; // 编译错误数组在运行时知道元素类型,泛型在运行时被擦除,两者机制冲突。
陷阱4:通配符方向写反
java
List<? extends Number> producer = List.of(1, 2, 3);
// producer.add(4); // 不能安全写入
List<? super Integer> consumer = new ArrayList<Number>();
consumer.add(1); // 可以写入 Integer记住 PECS:读取用 extends,写入用 super。
陷阱5:运行时判断泛型参数
java
// if (obj instanceof List<String>) { } // 编译错误
if (obj instanceof List<?>) {
System.out.println("is a list");
}运行时泛型参数大多被擦除,只能判断 List<?>。
🆚 九、Java vs C 对比
| 特性 | C | Java 泛型 |
|---|---|---|
| 泛型能力 | 通常用宏或 void* 模拟 | 语言级类型参数 |
| 类型检查 | 宏弱检查,void* 靠人工约定 | 编译期检查 |
| 运行时信息 | 结构体指针无泛型信息 | 类型擦除,大多无泛型实参 |
| 容器类型 | 手写或库实现 | 集合框架广泛使用泛型 |
对 C 程序员来说,Java 泛型可以理解为“编译期类型安全的容器模板”,但它不是 C++ 模板。Java 泛型不会为 List<String> 和 List<Integer> 生成两套运行时代码,而是通过类型擦除复用同一份实现。
📖 十、泛型擦除后的桥接方法
泛型擦除可能生成桥接方法,保证多态仍然成立。
示例:
java
class StringBox implements Comparable<StringBox> {
@Override
public int compareTo(StringBox other) {
return 0;
}
}擦除后接口方法接近:
java
int compareTo(Object o);编译器会生成桥接方法,把 Object 转成 StringBox 后调用真正方法。
这解释了为什么反射查看某些类时,会看到 bridge、synthetic 方法。
📖 十一、泛型 API 设计建议
设计泛型方法时,先判断类型参数是否真的表达约束。
好例子:
java
public static <T> T first(List<T> list) {
return list.get(0);
}类型参数 T 同时出现在参数和返回值中,表达“返回值类型与列表元素类型一致”。
坏例子:
java
public static <T> void print(Object value) {
System.out.println(value);
}T 没有提供任何约束,应删除。
通配符示例:
java
public static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}如果只读取,不关心元素具体类型,List<?> 比 <T> 更直接。
✅ 十二、掌握标准
学完本课后,应能做到:
text
能定义泛型类、泛型接口、泛型方法。
能解释泛型为什么提升类型安全。
能说明类型擦除带来的限制。
能区分 T、E、K、V 等常用命名。
能使用 extends 上界限定。
能正确使用 ?、? extends、? super。
能用 PECS 判断读写方向。
能避免原始类型和泛型数组陷阱。
能理解 Java 泛型与 C 宏/void* 的根本区别。泛型的核心价值是把类型错误提前到编译期。越靠近公共 API,越应该认真设计泛型边界。
📝 练习
完成 练习/Ex18_Generics.java:
- 泛型类实现
- 泛型方法
- 类型限定
- 通配符使用
- 泛型集合
- 综合:通用缓存系统
🎓 下一步
- 第19课:IO流 - 字节流 - InputStream、OutputStream