杰瑞科技汇

arraylist泛型 java

什么是泛型?

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

arraylist泛型 java-图1
(图片来源网络,侵删)

泛型,即“参数化类型”,是 Java 5 引入的一个特性,它的核心思想是在定义类、接口或方法时,不指定具体的数据类型,而是使用一个类型参数作为占位符,当使用这个类、接口或方法时,再传入具体的类型。

泛型的主要目的:

  1. 类型安全:在编译阶段进行类型检查,防止将不兼容类型的对象存入集合中,避免在运行时出现 ClassCastException
  2. 避免强制类型转换:从集合中取出元素时,无需进行繁琐且不安全的强制类型转换,代码更简洁、可读性更高。

为什么 ArrayList 需要泛型?

在没有泛型的早期 Java 版本中,ArrayList 的定义是这样的:

// 旧版非泛型 ArrayList
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器允许,但非常危险

这样做的问题很明显:

arraylist泛型 java-图2
(图片来源网络,侵删)
  • 类型不安全list 可以存放任何 Object 类型的对象,StringInteger 可以混存。
  • 运行时异常风险:当从 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);
        }
    }
}

代码解读:

arraylist泛型 java-图3
(图片来源网络,侵删)
  • List<String> 声明了一个 List 接口变量,它只能引用 String 类型的列表。
  • new ArrayList<>() 创建了一个底层的 ArrayList 实例,并且这个实例被约束为只能存储 String 对象。
  • 当你尝试添加一个 Integer 时,编译器会立即报错,避免了潜在的运行时错误。
  • 当你从 stringList 中取出元素时,它的类型就是 String,无需任何强制转换。

ArrayList 泛型与原始类型

原始类型就是不使用泛型的 ArrayListArrayList

// 原始类型,不推荐使用
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 代码的基石,请务必在编码中始终使用泛型,并理解通配符的用法,以写出更灵活的代码。

分享:
扫描分享到社交APP
上一篇
下一篇