泛型(Generics)是 Java 5 引入的核心特性,它让代码更安全、更灵活、更易于复用。本文从“为什么需要泛型”出发,系统讲解泛型的概念、语法、通配符、类型擦除及实际应用场景,助你彻底掌握 Java 类型系统的精髓。
一、为什么需要泛型?—— 告别 Object
的“野蛮时代”
在泛型出现之前,Java 集合类(如 ArrayList
、HashMap
)使用 Object
存储元素:
// 泛型前:一个充满风险的 List
List list = new ArrayList();
list.add("Hello");
list.add(100); // 编译通过,但埋下隐患
String str = (String) list.get(0); // 强制转型
Integer num = (Integer) list.get(1); // 运行时抛出 ClassCastException!
问题:
-
类型不安全:任何类型都能插入,编译期无法检查。
-
繁琐的强制转型:取元素时必须手动向下转型。
-
运行时异常风险:类型错误在运行时才暴露。
二、泛型是什么?—— 编译期的“类型契约”
泛型本质:将类型参数化,让类、接口、方法在定义时不指定具体类型,而在使用时再指定。
// 泛型拯救世界!
List<String> strList = new ArrayList<>(); // 指定元素类型为 String
strList.add("Java");
strList.add(100); // ❌ 编译错误!直接拒绝非String类型
String first = strList.get(0); // ✅ 无需强制转型,自动类型推断
核心价值:
-
✅ 类型安全:编译期检查类型一致性。
-
✅ 消除强制转型:代码更简洁、可读性更强。
-
✅ 代码复用:一份代码处理多种数据类型(如
List<T>
)。
三、泛型基础语法—— 类、接口、方法
1. 泛型类(Generic Class)
// 定义一个泛型容器
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
// 使用
Box<String> stringBox = new Box<>();
stringBox.setContent("Generics");
String value = stringBox.getContent(); // 直接返回String
2. 泛型接口(Generic Interface)
public interface Repository<T> {
void save(T entity);
T findById(int id);
}
// 实现类指定具体类型
public class UserRepository implements Repository<User> {
@Override
public void save(User user) { ... }
@Override
public User findById(int id) { ... }
}
3. 泛型方法(Generic Method)
方法自己声明类型参数(可独立于类)
public class Utils {
// 静态泛型方法:交换数组两个元素
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// 调用:类型自动推断
Integer[] nums = {1, 2, 3};
Utils.swap(nums, 0, 2); // 无需显式指定<Integer>
四、通配符 ?
—— 泛型的“灵活控制器”
通配符 ?
用于表示未知类型,增强泛型的灵活性,常用于方法参数。
1. 上界通配符 <? extends T>
接受
T
或其子类(生产者场景,适合读取)
// 打印所有Shape及其子类(Circle, Rectangle)的列表
public void drawAll(List<? extends Shape> shapes) {
for (Shape shape : shapes) {
shape.draw();
}
}
2. 下界通配符 <? super T>
接受
T
或其父类(消费者场景,适合写入)
// 将Circle对象放入一个“元素是Circle父类”的集合
public void addCircle(List<? super Circle> list) {
list.add(new Circle());
}
3. 无界通配符 <?>
表示完全未知类型(极少用,可被
<T>
替代)
// 仅关心集合操作,不依赖元素类型
public void printSize(List<?> list) {
System.out.println(list.size());
}
五、类型擦除 —— 泛型的“幕后真相”
Java 泛型是编译期特性,运行时类型信息会被擦除(Backward Compatibility)。
擦除规则:
-
泛型类/接口 → 原始类型(Raw Type),如
Box<T>
→Box
-
类型参数
T
→ 其上界(默认Object
) -
必要时插入强制转型和类型检查
// 编译前
List<String> list = new ArrayList<>();
list.add("Hi");
String s = list.get(0);
// 编译后(等效代码)
List list = new ArrayList(); // 原始类型
list.add("Hi");
String s = (String) list.get(0); // 编译器插入转型
擦除带来的限制:
-
不能创建泛型数组:
new T[size]
❌ → 用ArrayList
替代。 -
无法使用
instanceof T
:运行时类型已被擦除。 -
静态成员不能使用泛型类型:静态域/方法属于类,与实例无关。
六、泛型最佳实践 ✅
-
优先使用泛型集合:
List<String>
而非List
。 -
避免混用原始类型(Raw Type):如
new ArrayList()
会绕过类型检查。 -
PECS 原则(Producer-Extends, Consumer-Super):
-
从集合读取 →
<? extends T>
-
向集合写入 →
<? super T>
-
-
限制泛型上界:
<T extends Comparable<T>>
确保 T 可比较。 -
利用类型推断:多写
<>
(钻石操作符),少写冗余类型。
七、典型应用场景
-
集合框架:
ArrayList<E>
,HashMap<K,V>
的核心基石。 -
函数式接口:
Comparator<T>
、Predicate<T>
。 -
工具类设计:如
Collections.sort()
,Optional<T>
。 -
通用算法:如排序、查找、缓存等可复用逻辑。