一、泛型是什么?为什么需要它?
泛型概述
泛型是“类型参数
”的一种机制,允许在类
、接口
或方法
中使用未知的通用类型。它是在 JDK 5 中引入的,提供了编译时的类型安全检查,避免了运行时错误。
参数化类型:将数据类型作为参数化
,使用时指定具体类型。
格式:
<类型>:单个类型参数。
<类型1, 类型2, ...>:多个类型参数。
场景案例:想象你有一个魔法储物箱,放入苹果只能取出苹果,放入橘子只能取出橘子。泛型就是给代码加上这种"类型标签
"的能力。
// 没有泛型的痛苦(需要手动强制转换)
List list = new ArrayList();
list.add("Hello");
String str = (String) list.get(0); // 需要强制转换
// 使用泛型的优雅
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 自动识别类型
在这个示例中,list 仅能存储String 类型,编译时会检查类型安全。
核心价值:
- 🛡️ 编译期类型检查:提前发现类型错误
- 🚫 避免强制转换:代码更简洁安全
- 📦 代码复用:一套代码支持多种数据类型
二、泛型基础语法速查表
类型 | 语法示例 | 说明 |
---|---|---|
泛型类 | class Box<T> { … } | 类名后声明类型参数 |
泛型接口 | interface List<E> { … } | 接口名后声明类型参数 |
泛型方法 | <T> T getData(Class<T> clazz) | 方法返回值前声明类型参数 |
通配符 | List<?> | 未知类型的占位符 |
三、3分钟上手:泛型实战
1. 自定义泛型结构
泛型类和接口
- 泛型类可以有多个类型参数,例如
<E1, E2, E3>
。 - 泛型类的构造器不需指定类型,例如
public GenericClass()
。 - 实例化时,泛型类型必须一致,不能相互赋值。
泛型类定义
public class Generic<T> { }
T 可以是任意标识,常用的还有 E、K、V 等。
注意事项
- 引用不相互赋值:ArrayList 和 ArrayList 是不同类型,尽管在运行时只加载一个 ArrayList 到 JVM 中。
泛型擦除:如果未指定泛型,编译后会被擦除为 Object 类型,需一致使用泛型。 - 接口和抽象类:不能创建泛型接口或抽象类的对象。
- 简化操作:在 JDK 1.7 后,泛型可简化为 ArrayList flist = new ArrayList<>();
- 使用包装类:泛型中不能使用基本数据类型,需使用对应的包装类。
- 泛型声明:在类或接口中声明的泛型只能用于非静态属性和方法,静态方法中不能使用。
- 异常类:异常类不能是泛型。
- 数组创建:不能直接使用 new E[],但可以使用 E[] elements = (E[]) new Object[capacity];
- 子类泛型:子类可选择保留或指定父类的泛型,保留时可增加自己的泛型。
子类不保留父类的泛型:按需实现
没有类型 擦除
具体类型
子类保留父类的泛型:泛型子类全部保留
部分保留
结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型
示例
public class MagicBox<T> {
private T content;
public void put(T item) {
this.content = item;
}
public T get() {
return content;
}
public static void main(String[] args) {
MagicBox<String> box1 = new MagicBox<>();
box1.put("神秘礼物");
String gift = box1.get(); // 无需强制转换
MagicBox<Integer> box2 = new MagicBox<>();
box2.put(100);
int money = box2.get();
}
}
泛型方法(万能转换器)
泛型方法的参数与类的泛型无关,可以声明为静态,因其参数在调用时确定。
示例
public class Converter {
// 将数组转换为List(支持任意类型)
public static <T> List<T> arrayToList(T[] array) {
return Arrays.asList(array);
}
public static void main(String[] args) {
String[] arr = {"A", "B", "C"};
List<String> list = arrayToList(arr); // 自动推断类型
}
}
使用泛型的总结
- 集合类在 JDK 5 中引入泛型。
- 实例化时可指定泛型类型。
- 指定后,内部结构需与指定类型一致。
- 泛型类型必须是类,不能是基本数据类型。
- 未指定泛型时,默认类型为 java.lang.Object。
四、高级技巧:类型通配符
1. 通配符类型对比表
通配符 | 含义 | 允许操作 |
---|---|---|
List<?> | 未知类型列表,可以匹配任何类型 | 只能读取为Object |
List<? extends Number> 上限通配符 | 必须是Number子类(如Integer/Double) | 安全读取为Number |
List<? super Integer> 下限通配符 | 必须是Integer父类(如Number/Object) | 可以安全添加Integer对象 |
2. 使用示例
// 打印所有数字(支持Integer/Double等)
public static void printNumbers(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num.doubleValue());
}
}
// 添加整数到容器(支持List<Number>/List<Object>)
public static void addInteger(List<? super Integer> list) {
list.add(100);
}
五、必须知道的注意事项
1. 类型擦除机制:
在 Java 虚拟机中,没有泛型类型对象,所有对象都被视为普通类。编译和运行后
,类型变量会被擦除并替换为原始类型(Raw type)。擦除规则是用最近的上界
替换类型参数,如果没有上界,则替换为 Object。
泛型是在 JDK 1.5 中引入的,类型擦除的目的是保持与旧版本的兼容性
,确保已有代码正常运行。
泛型在继承中的体现
尽管类 A 是类 B 的父类,G 和 G 之间并不具备子父类关系,而是平行关系。补充说明:A
总结:
- 编译后泛型类型会被擦除(
MagicBox<String> → MagicBox
) - 运行时无法获取泛型具体类型
示例:以下代码会编译报错
if (list instanceof ArrayList<String>) { ... } // 错误!
2. 泛型数组限制:
// 错误写法(编译不通过)
T[] arr = new T[10];
// 正确替代方案
T[] arr = (T[]) new Object[10]; // 需注意类型安全
- PECS原则(Producer-Extends, Consumer-Super):
- 从数据结构获取数据时用
extends
(生产者) - 往数据结构存入数据时用
super
(消费者)
六、常见问题解答
Q1:泛型能用基本类型吗?
❌ 不能!必须用包装类:
List<int> list = new ArrayList<>(); // 错误!
List<Integer> list = new ArrayList<>(); // 正确
Q2:List和List<?>有什么区别?
- List:明确存储Object类型
- List<?>:未知类型(更安全的API设计选择)
Q3:如何实现泛型接口?
interface Generator<T> {
T next();
}
// 实现方式1:指定具体类型
class StringGenerator implements Generator<String> {
@Override
public String next() { return "Hello"; }
}
// 实现方式2:保持泛型
class BoxGenerator<T> implements Generator<T> {
@Override
public T next() { return null; }
}
七、总结:泛型核心要点
- ✅ 安全:编译期类型检查
- ✅ 灵活:一套代码支持多种类型
- ✅ 清晰:代码可读性更强
- ❗ 限制:不能使用基本类型、运行时类型擦除
最佳实践口诀:
- 类型参数声明尖括号,
- 安全转换从此不说No,
- 通配符分上下界限,
- PECS原则牢记心间。