Java中的协变、逆变

在 Java 中,协变(Covariance)逆变(Contravariance) 是泛型系统中的重要概念,用于描述子类型如何在复杂类型(如集合)中传递。

1. 基础概念:什么是协变和逆变?

假设 BA 的子类型(例如 IntegerNumber 的子类型),那么:

  • 协变List<B>List<A> 的子类型(允许从子类型向父类型转换)。
  • 逆变List<A>List<B> 的子类型(允许从父类型向子类型转换)。
  • 不变List<B>List<A> 之间没有继承关系(不允许直接转换)。

Java 中的规则

  • 数组是协变的,但可能导致运行时错误。
  • 泛型是不变的,但可以通过通配符(? extends T? super T)实现有限的协变和逆变。

2. 数组的协变(Java 中的特殊情况)

Java 的数组是协变的,即如果 BA 的子类,则 B[] 可以赋值给 A[]。但这可能导致运行时异常:

Number[] numbers = new Integer[5]; // 合法,数组是协变的
numbers[0] = 3.14; // 运行时错误:ArrayStoreException(数组类型不匹配)

类比
想象一个 “水果篮子”(Fruit[]),可以放入苹果(Apple[])或香蕉(Banana[])。但如果篮子实际装的是苹果(new Apple[5]),你却试图放入香蕉(Banana),就会报错。

3. 泛型的不可协变性(不变性)

Java 的泛型是不可协变的,即 List<B> 不是 List<A> 的子类型,即使 BA 的子类。例如:

List<Number> numbers = new ArrayList<Integer>(); // 编译错误!

为什么泛型不可协变?
如果允许这样的赋值,会破坏类型安全。例如:

List<Integer> ints = new ArrayList<>();
ints.add(10);

// 假设允许 List<Integer> 赋值给 List<Number>
List<Number> nums = ints; // 编译错误(实际不允许)
nums.add(3.14); // 如果允许,这里会向 List<Integer> 中添加 Double!
Integer i = ints.get(0); // 运行时 ClassCastException!

类比
想象一个 “苹果篮子”(List<Apple>)和一个 “水果篮子”(List<Fruit>)。虽然苹果是水果的一种,但 “苹果篮子”不是“水果篮子” 的子类。如果允许将 “苹果篮子” 当作 “水果篮子”,就可能向其中放入香蕉,导致类型错误。

4. 泛型通配符实现有限的协变和逆变

虽然泛型本身不可协变,但可以通过通配符实现安全的类型转换:

(1)上限通配符 ? extends T(协变)

允许读取 T 类型的元素,但不能写入(除了 null):

List<? extends Number> numbers = new ArrayList<Integer>(); // 合法
Number num = numbers.get(0); // 合法,读取为 Number
// numbers.add(10); // 编译错误,不能添加任何类型(除了 null)

作用:安全地从集合中读取元素(生产者)。
类比
有一个 “水果篮子”(List<? extends Fruit>),你知道里面装的是水果(或其子类),但不确定具体是哪种水果。因此,你可以安全地从中取出水果(作为 Fruit 类型),但不能向其中添加任何水果(因为不知道篮子的具体类型)。

(2)下限通配符 ? super T(逆变)

允许写入 T 类型的元素,但读取时只能作为 Object

List<? super Integer> numbers = new ArrayList<Number>(); // 合法
numbers.add(10); // 合法,可添加 Integer
Object obj = numbers.get(0); // 读取为 Object

作用:安全地向集合中写入元素(消费者)。
类比
有一个 “篮子”(List<? super Apple>),你知道它可以装苹果(或其父类,如水果、食物)。因此,你可以安全地向其中放入苹果(Apple),但取出时只能当作 “物品”(Object),因为不确定篮子的具体类型。

5. 协变与逆变的对比表

场景数组(协变)泛型(不变)泛型通配符(有限协变 / 逆变)
赋值Number[] = Integer[](合法)List<Number> = List<Integer>(编译错误)List<? extends Number> = List<Integer>(合法)
写入元素可能运行时错误编译错误? extends T:禁止写入 ? super T:允许写入 T
读取元素安全安全? extends T:读取为 T ? super T:读取为 Object
类型安全运行时可能出错编译阶段强制检查编译阶段保证安全

6. 总结:何时使用协变和逆变?

  • 协变(? extends T):当你只需要从集合中读取元素时使用。例如:

    // 安全地读取 List<Integer> 中的元素
    List<? extends Number> nums = List.of(1, 2, 3);
    Number n = nums.get(0); // 合法
    
  • 逆变(? super T):当你只需要向集合中

    // 安全地向 List<Object> 中添加 Integer
    List<? super Integer> nums = new ArrayList<>();
    nums.add(10); // 合法
    

通过通配符,Java 泛型在保证类型安全的前提下,提供了一定的灵活性。理解协变和逆变是掌握 Java 泛型高级用法的关键。

7. 使用场景

(1)只读场景:使用 ? extends T(协变)

如果你只需要从集合中读取元素,使用上限通配符 ? extends T
示例:计算数字集合的总和:

public double sum(List<? extends Number> numbers) {
    double sum = 0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}

// 可以传入 List<Integer> 或 List<Double>
List<Integer> ints = List.of(1, 2, 3);
sum(ints); // 合法

List<Double> doubles = List.of(1.5, 2.5);
sum(doubles); // 合法

限制:不能向 List<? extends Number> 中添加元素(除了 null),因为无法确定具体类型。

(2)只写场景:使用 ? super T(逆变)

如果你只需要向集合中写入元素,使用下限通配符 ? super T
示例:将整数添加到集合中:

public void addNumbers(List<? super Integer> numbers) {
    for (int i = 0; i < 10; i++) {
        numbers.add(i); // 合法:可以添加 Integer 类型
    }
}

// 可以传入 List<Integer>、List<Number> 或 List<Object>
List<Number> nums = new ArrayList<>();
addNumbers(nums); // 合法

限制:从 List<? super Integer> 中读取元素时,只能作为 Object 类型,因为无法确定具体类型。

(3)读写兼顾场景:使用不变(无通配符)

如果你需要对集合进行读写操作,则不使用通配符,直接指定具体类型。
示例:操作整数列表:

public void processIntegers(List<Integer> ints) {
    // 读取元素
    int first = ints.get(0);
    
    // 写入元素
    ints.add(first * 2);
}

// 只能传入 List<Integer>,不能传入 List<Number> 或 List<Object>
List<Integer> ints = new ArrayList<>();
processIntegers(ints); // 合法

限制:类型必须精确匹配,不允许类型转换。

8. 常见误区与注意事项

  1. 数组的协变陷阱
    数组的协变可能导致运行时异常(ArrayStoreException),应尽量避免将子类数组赋值给父类数组变量。

  2. 通配符与子类的区别
    List<? extends Number> 不代表 “所有 Number 子类的列表”,而是 “某个具体子类的列表”。例如:

    List<? extends Number> nums = new ArrayList<Integer>(); // 合法
    // nums.add(3.14); // 编译错误:无法确定具体类型
    
  3. 逆变的读取限制
    List<? super Integer> 中读取的元素只能作为 Object,因为无法确定集合的具体类型。

  4. 泛型方法 vs 通配符
    如果需要在多个参数或返回值之间建立类型关联,优先使用泛型方法而非通配符。例如:

    <T> void swap(List<T> list, int i, int j); // 泛型方法,保证两个索引位置的元素类型一致
    

9. 实战案例

案例 1:生产者场景(读取数据)
// 计算集合中所有数字的平均值
public double average(List<? extends Number> numbers) {
    double sum = 0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum / numbers.size();
}
案例 2:消费者场景(写入数据)
// 将元素添加到集合
public void fill(List<? super String> list, String value, int count) {
    for (int i = 0; i < count; i++) {
        list.add(value); // 合法:可以添加 String
    }
}
案例 3:读写兼顾场景(精确类型)
// 对列表中的每个元素执行操作
public void transform(List<Integer> list, Function<Integer, Integer> mapper) {
    for (int i = 0; i < list.size(); i++) {
        list.set(i, mapper.apply(list.get(i))); // 读写操作,必须精确类型
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值