什么是泛型?
在深入 ArrayList 泛型之前,我们先理解什么是泛型。

泛型,即“参数化类型”,是 Java 5 引入的一个特性,它的核心思想是在定义类、接口或方法时,不指定具体的数据类型,而是使用一个类型参数作为占位符,当使用这个类、接口或方法时,再传入具体的类型。
泛型的主要目的:
- 类型安全:在编译阶段进行类型检查,防止将不兼容类型的对象存入集合中,避免在运行时出现
ClassCastException。 - 避免强制类型转换:从集合中取出元素时,无需进行繁琐且不安全的强制类型转换,代码更简洁、可读性更高。
为什么 ArrayList 需要泛型?
在没有泛型的早期 Java 版本中,ArrayList 的定义是这样的:
// 旧版非泛型 ArrayList
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器允许,但非常危险
这样做的问题很明显:

- 类型不安全:
list可以存放任何Object类型的对象,String和Integer可以混存。 - 运行时异常风险:当从
list中取元素时,如果程序员不小心,可能会进行错误的类型转换,导致ClassCastException。
String str = (String) list.get(0); // 正常,返回 "Hello" String anotherStr = (String) list.get(1); // 编译通过,但运行时抛出 ClassCastException!
为了解决这些问题,ArrayList 引入了泛型。
ArrayList 泛型的基本用法
泛型 ArrayList 的基本语法是在类名 ArrayList 后面用尖括号 <> 指定要存储的元素类型。
语法
ArrayList<数据类型> list = new ArrayList<数据类型>();
从 Java 7 开始,支持“菱形操作符” (Diamond Operator),可以简化代码,让右侧的泛型类型从左侧推断出来。
// Java 7+ 推荐写法 ArrayList<数据类型> list = new ArrayList<>();
示例:创建一个只能存储 String 类型的 ArrayList
import java.util.ArrayList;
import java.util.List;
public class GenericArrayListExample {
public static void main(String[] args) {
// 1. 创建一个只能存储 String 对象的 ArrayList
List<String> stringList = new ArrayList<>();
// 2. 添加元素
stringList.add("Apple");
stringList.add("Banana");
stringList.add("Orange");
// 3. 编译器会在这里进行类型检查,以下代码会编译失败!
// stringList.add(100); // Error: incompatible types: int cannot be converted to String
// 4. 获取元素
String fruit = stringList.get(0); // 无需强制类型转换,直接就是 String 类型
System.out.println("First fruit: " + fruit);
// 5. 遍历集合 (使用增强 for 循环)
System.out.println("--- All fruits ---");
for (String s : stringList) {
System.out.println(s);
}
}
}
代码解读:

List<String>声明了一个List接口变量,它只能引用String类型的列表。new ArrayList<>()创建了一个底层的ArrayList实例,并且这个实例被约束为只能存储String对象。- 当你尝试添加一个
Integer时,编译器会立即报错,避免了潜在的运行时错误。 - 当你从
stringList中取出元素时,它的类型就是String,无需任何强制转换。
ArrayList 泛型与原始类型
原始类型就是不使用泛型的 ArrayList,ArrayList。
// 原始类型,不推荐使用
ArrayList rawList = new ArrayList();
rawList.add("Hello");
rawList.add(100);
为什么应该避免使用原始类型?
使用原始类型会绕过编译器的类型检查,使得代码失去了泛型带来的所有好处(类型安全和免转换),它和非泛型的旧代码兼容,但在新代码中应该尽量避免。
当你将一个使用了泛型的 ArrayList 赋值给一个原始类型的 ArrayList 引用时,会收到一个“未检查的调用” (unchecked call) 警告。
ArrayList<String> stringList = new ArrayList<>(); ArrayList rawList = stringList; // 警告: [unchecked] unchecked conversion
泛型通配符
在处理泛型时,我们经常会遇到通配符 ,它主要用于方法参数,以增加方法的灵活性。
1 无界通配符
表示 “ 代表任意类型”。
使用场景:当你只想使用 ArrayList 的方法,而不关心或不需要关心其内部存储的具体类型时(计算大小、打印内容)。
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
// 调用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> strList = Arrays.asList("A", "B", "C");
printList(intList); // 可以
printList(strList); // 可以
注意:在 printList 方法内部,你不能向 list 中添加任何元素,因为编译器不知道 具体是什么类型,为了保证类型安全,它会禁止添加操作。
public void addElement(List<?> list) {
// list.add("Hello"); // 编译错误: incompatible types: String cannot be converted to CAP#1
// list.add(123); // 编译错误: incompatible types: int cannot be converted to CAP#1
}
2 上限通配符 ? extends T
表示 “ 是 T 类型或 T 的子类”。
使用场景:表示这个集合可以读取 T 或其子类的元素,并且是只读的(生产者原则, PECS: Producer Extends)。
// T 代表 Number 或其子类,如 Integer, Double, Float
public double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // 安全,因为 Number 有 doubleValue() 方法
}
return sum;
}
// 调用
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
System.out.println(sumOfList(intList)); // 可以
System.out.println(sumOfList(doubleList)); // 可以
注意:你不能向 List<? extends Number> 中添加任何 Number 或其子类的对象,因为编译器不知道这个 List 的底层数据类型究竟是 Integer 还是 Double,如果允许添加 Integer,而列表实际是 List<Double>,就会导致类型不匹配。
List<Integer> intList = Arrays.asList(1, 2, 3); List<? extends Number> numberList = intList; // numberList.add(10); // 编译错误: incompatible types: Integer cannot be converted to CAP#1
3 下限通配符 ? super T
表示 “ 是 T 类型或 T 的父类”。
使用场景:表示这个集合可以添加 T 或其父类的元素,并且是只写的(消费者原则, PECS: Consumer Super)。
// T 代表 Integer 或其父类,如 Number, Object
public void addNumbers(List<? super Integer> list) {
list.add(10); // 安全,因为 Integer 是 Integer, Number, Object 的子类
list.add(20);
// list.add(3.14); // 编译错误: incompatible types: Double cannot be converted to CAP#1
}
// 调用
List<Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
addNumbers(intList); // 可以
addNumbers(numberList); // 可以
addNumbers(objList); // 可以
注意:从 List<? super Integer> 中读取元素时,你只能确定它是 Object 类型。
List<? super Integer> list = new ArrayList<>(); list.add(10); Object obj = list.get(0); // 只能安全地当作 Object 处理 // Integer i = list.get(0); // 编译错误: incompatible types: Object cannot be converted to Integer
ArrayList 泛型的内部实现原理
ArrayList 的泛型是通过类型擦除 来实现的。
- 编译时:编译器会检查泛型类型是否正确,如果正确,它会生成与没有泛型时几乎一样的字节码,在字节码中,泛型类型信息(
<String>,<Integer>)会被“擦除”,变成它的原始类型(对于ArrayList来说就是Object)。 - 运行时:JVM 看不到任何泛型类型信息。
ArrayList<String>和ArrayList<Integer>在运行时都是ArrayList。
这意味着什么?
ArrayList<String> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); // 在运行时,list1 和 list2 的类型是一样的 System.out.println(list1.getClass() == list2.getClass()); // 输出 true
类型擦除也带来了一些限制,比如不能使用 new T() 或 instanceof T 这样的泛型类型操作。
| 特性 | 描述 | 示例 |
|---|---|---|
| 基本定义 | 在 ArrayList 后使用 <> 指定元素类型。 |
ArrayList<String> |
| 类型安全 | 编译器检查,防止存入错误类型。 | list.add(123); // 在 ArrayList<String> 中会报错 |
| 免转换 | 取出元素时类型已确定,无需强制转换。 | String s = list.get(0); |
| 原始类型 | 不使用泛型,应避免。 | ArrayList list = new ArrayList(); |
| 无界通配符 | 表示任意类型,用于只读操作。 | void print(List<?> list) |
上限通配符 ? extends T |
表示 T 或其子类,用于读取(生产者)。 |
double sum(List<? extends Number> list) |
下限通配符 ? super T |
表示 T 或其父类,用于写入(消费者)。 |
void add(List<? super Integer> list) |
| 实现原理 | 类型擦除,编译后泛型信息丢失,变为原始类型。 | ArrayList<String> 和 ArrayList<Integer> 在运行时都是 ArrayList |
掌握 ArrayList 泛型是编写健壮、安全和可维护 Java 代码的基石,请务必在编码中始终使用泛型,并理解通配符的用法,以写出更灵活的代码。
