Skip to content

泛型在集合中的使用与常见陷阱

概述

Java 集合框架(List、Set、Map 等)从 JDK 5 起全面支持泛型。在集合上使用泛型,可以在编译期就约束元素类型,避免放入错误类型、取用时无需强转,并减少 ClassCastException。本文介绍泛型在集合中的正确用法,以及类型擦除、原始类型、泛型数组等带来的常见陷阱与规避方式。


泛型在集合中的基本用法

声明带泛型的集合

集合接口和实现类都接受类型参数List<E>Set<E>Map<K, V>。声明时指定具体类型,编译器会据此做类型检查。

java
import java.util.*;

// List:元素类型为 String
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
String first = names.get(0);   // 无需强转,类型为 String

// Set:元素类型为 Integer
Set<Integer> ids = new HashSet<>();
ids.add(1);
ids.add(2);
for (Integer id : ids) {
    System.out.println(id.intValue());
}

// Map:键为 String,值为 Integer
Map<String, Integer> scores = new HashMap<>();
scores.put("语文", 90);
scores.put("数学", 85);
int math = scores.get("数学");   // 返回类型为 Integer,自动拆箱为 int

菱形语法(类型推断)

从 JDK 7 开始,构造泛型类时可以省略右侧的类型参数,只保留 <>(菱形),编译器会根据左侧变量类型推断。

java
// JDK 7+ 推荐:右侧使用菱形,类型由左侧推断
List<String> list = new ArrayList<>();
Map<String, List<Integer>> map = new HashMap<>();

提示

声明时始终在变量类型上写上泛型(如 List<String>),右侧用 new ArrayList<>() 即可,既简洁又类型安全。


常见用法示例

示例 1:嵌套泛型

集合中再放集合时,类型参数可以嵌套,注意书写清晰。

java
// 班级 -> 学生姓名列表
Map<String, List<String>> classToStudents = new HashMap<>();
classToStudents.put("一班", List.of("小明", "小红"));
classToStudents.put("二班", List.of("小刚", "小丽"));

for (Map.Entry<String, List<String>> entry : classToStudents.entrySet()) {
    String className = entry.getKey();
    List<String> students = entry.getValue();
    System.out.println(className + ": " + students);
}

示例 2:泛型与 for-each

使用泛型后,for-each 循环中的变量类型就是元素类型,无需强转。

java
List<String> fruits = List.of("苹果", "香蕉", "橙子");
for (String fruit : fruits) {
    System.out.println(fruit.toUpperCase());  // String 的方法可直接调用
}

Map<String, Integer> map = Map.of("a", 1, "b", 2);
for (Map.Entry<String, Integer> e : map.entrySet()) {
    String k = e.getKey();
    Integer v = e.getValue();
}

示例 3:泛型方法配合集合

方法参数或返回值是集合时,用泛型方法可以保持类型一致、避免原始类型。

java
// 安全地取 List 的第一个元素,类型为 T
public static <T> T getFirst(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

// 使用示例
List<String> names = List.of("A", "B", "C");
String first = getFirst(names);   // 返回 String,无需强转

List<Integer> numbers = List.of(1, 2, 3);
Integer n = getFirst(numbers);    // 返回 Integer

常见陷阱与规避

陷阱 1:使用原始类型(Raw Type)

未带类型参数的集合称为原始类型(如 ListMap)。编译器会给出「未检查」警告,且取出的元素是 Object,容易在强转时发生运行时错误。

java
// 不推荐:原始类型,编译器会警告
List list = new ArrayList();
list.add("hello");
list.add(100);
String s = (String) list.get(1);  // 运行时 ClassCastException!

注意

不要使用原始类型的集合。应始终写成 List<String>Map<K, V> 等形式,在编译期就发现类型错误。

正确写法:声明时指定具体类型参数。

java
List<String> list = new ArrayList<>();
list.add("hello");
list.add(100);           // 编译错误:List<String> 不能放 Integer
String s = list.get(0);  // 无需强转

陷阱 2:类型擦除导致的限制

Java 泛型采用类型擦除实现:编译后泛型信息被擦除,运行时 List<String>List<Integer> 都只是 List。因此会有以下限制。

不能用于 instanceof、强转等需要「具体泛型类型」的场景

java
// 编译错误:不能对泛型做 instanceof 检查
if (obj instanceof List<String>) { }

// 运行时 list 的泛型信息已擦除,强转可能掩盖错误
List<String> list = new ArrayList<>();
List other = list;
List<Integer> intList = (List<Integer>) other;  // 编译通过,但危险!

不能直接 new 泛型类型的数组(可改用 ListArray.newInstance 等):

java
// 编译错误:不能创建带泛型类型参数的数组
// T[] arr = new T[10];

// 可行:使用 List 代替「泛型数组」
public <T> List<T> createList() {
    return new ArrayList<>();
}

版本说明

JDK 未提供「可具体化的泛型」;若需要运行时保留类型信息,可考虑在方法中传入 Class<T> 或使用 Gson/Jackson 等库的 TypeToken 等机制。


陷阱 3:泛型与数组的混用

List<String>[] 这类「元素类型为参数化类型的数组」可以声明,但不建议使用,容易踩坑。更稳妥的做法是用 List<List<String>> 等集合代替「泛型数组」。

java
// 不推荐:泛型数组易导致未检查警告和潜在类型错误
@SuppressWarnings("unchecked")
List<String>[] arrayOfLists = new List[3];

// 推荐:用 List 的 List 代替
List<List<String>> listOfLists = new ArrayList<>();
listOfLists.add(new ArrayList<>(List.of("a", "b")));

陷阱 4:无界通配符与「只读」语义

List<?> 表示「某种类型的 List,但类型未知」。除 null 外,不能往 List<?> 里添加任意非 null 元素(编译器无法保证类型安全);只能读取,且读出的是 Object

java
List<?> list = new ArrayList<String>(List.of("a", "b"));
// list.add("c");     // 编译错误:不能向 List<?> 添加元素
// list.add(1);      // 同样错误
list.add(null);      // 唯一允许的「添加」
Object o = list.get(0);  // 只能当作 Object 使用

若既要读又要写,应使用具体类型(如 List<String>)或有界通配符(如 List<? extends Number> 只读、List<? super Integer> 可写 Integer)。详见 通配符


陷阱 5:遍历时修改集合导致 ConcurrentModificationException

使用 for-each 或 Iterator 遍历时,若通过集合自身remove()/add() 修改结构,可能抛出 ConcurrentModificationException。与泛型无直接关系,但在使用泛型集合时非常常见。

java
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
for (String s : list) {
    if ("B".equals(s)) {
        list.remove(s);  // 可能抛出 ConcurrentModificationException
    }
}

正确做法:需要「边遍历边删除」时使用 Iterator.remove()

java
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("B".equals(it.next())) {
        it.remove();
    }
}

提示

JDK 8+ 也可用 list.removeIf(s -> "B".equals(s)); 安全地按条件删除。


最佳实践小结

做法说明
始终为集合声明泛型使用 List<String>Map<K,V>,避免原始类型
右侧用菱形 <>new ArrayList<>(),由左侧推断类型
避免泛型数组List<T> 等集合代替 T[]
方法参数/返回值带泛型List<T> getFirst(List<T> list),保持类型一致
需要「只读」时用 ? extends T需要「只写」时用 ? super T,见 通配符
遍历中删除用 Iterator.remove() 或 removeIf()避免 ConcurrentModificationException

相关链接

基于 VitePress 构建