跳转至

Java 协变与逆变:深入理解与高效使用

简介

在 Java 编程中,协变(Covariance)和逆变(Contravariance)是两个重要的概念,它们涉及到泛型类型系统中类型的兼容性问题。理解这两个概念有助于我们更灵活地处理泛型集合,提高代码的复用性和可维护性。本文将详细介绍 Java 中协变与逆变的基础概念、使用方法、常见实践以及最佳实践,通过清晰的代码示例帮助读者深入掌握这两个概念。

目录

  1. 基础概念
    • 协变
    • 逆变
    • 不变性
  2. 使用方法
    • 数组的协变
    • 泛型的通配符
      • 上界通配符(协变)
      • 下界通配符(逆变)
  3. 常见实践
    • 协变的实践
    • 逆变的实践
  4. 最佳实践
    • 何时使用协变
    • 何时使用逆变
  5. 小结
  6. 参考资料

基础概念

协变

协变允许一个类型是另一个类型的子类型,在泛型或数组中,表现为子类型可以安全地赋值给父类型。例如,IntegerNumber 的子类型,在协变的情况下,List<Integer> 可以在某些情况下被视为 List<? extends Number>

逆变

逆变与协变相反,它允许一个类型是另一个类型的超类型。在泛型中,通过下界通配符实现,例如 List<? super Integer> 可以接受 List<Number>List<Object> 等。

不变性

Java 泛型默认是不变的,即 List<Integer> 不能直接赋值给 List<Number>,即使 IntegerNumber 的子类型。这是为了保证类型安全。

使用方法

数组的协变

Java 数组是协变的,这意味着可以将一个子类型数组赋值给一个父类型数组。

Integer[] intArray = new Integer[5];
Number[] numArray = intArray; // 数组的协变

但是需要注意,数组的协变可能会导致运行时异常:

Integer[] intArray = new Integer[5];
Number[] numArray = intArray;
numArray[0] = 3.14; // 运行时抛出 ArrayStoreException

泛型的通配符

上界通配符(协变)

上界通配符 <? extends T> 用于实现协变,它表示泛型类型是 TT 的子类型。

import java.util.ArrayList;
import java.util.List;

class Fruit {}
class Apple extends Fruit {}

public class CovarianceExample {
    public static void printFruits(List<? extends Fruit> fruits) {
        for (Fruit fruit : fruits) {
            System.out.println(fruit);
        }
    }

    public static void main(String[] args) {
        List<Apple> apples = new ArrayList<>();
        apples.add(new Apple());
        printFruits(apples); // 可以传递 List<Apple>
    }
}

下界通配符(逆变)

下界通配符 <? super T> 用于实现逆变,它表示泛型类型是 TT 的超类型。

import java.util.ArrayList;
import java.util.List;

class Fruit {}
class Apple extends Fruit {}

public class ContravarianceExample {
    public static void addApple(List<? super Apple> apples) {
        apples.add(new Apple());
    }

    public static void main(String[] args) {
        List<Fruit> fruitList = new ArrayList<>();
        addApple(fruitList); // 可以传递 List<Fruit>
    }
}

常见实践

协变的实践

协变常用于读取数据,当只需要从泛型集合中读取数据时,可以使用上界通配符。

import java.util.ArrayList;
import java.util.List;

class Animal {}
class Dog extends Animal {}

public class CovariancePractice {
    public static double sumAnimalsHeights(List<? extends Animal> animals) {
        // 这里可以安全地读取 Animal 对象
        return 0.0; // 示例代码,实际可以实现具体逻辑
    }

    public static void main(String[] args) {
        List<Dog> dogs = new ArrayList<>();
        sumAnimalsHeights(dogs);
    }
}

逆变的实践

逆变常用于写入数据,当需要向泛型集合中写入数据时,可以使用下界通配符。

import java.util.ArrayList;
import java.util.List;

class Animal {}
class Dog extends Animal {}

public class ContravariancePractice {
    public static void addDog(List<? super Dog> animalList) {
        animalList.add(new Dog());
    }

    public static void main(String[] args) {
        List<Animal> animalList = new ArrayList<>();
        addDog(animalList);
    }
}

最佳实践

何时使用协变

当方法只需要从泛型集合中读取数据,而不需要写入数据时,使用上界通配符(协变)。这样可以提高代码的灵活性,允许传递更具体的子类型集合。

何时使用逆变

当方法只需要向泛型集合中写入数据,而不需要读取数据时,使用下界通配符(逆变)。这样可以接受更通用的超类型集合。

小结

Java 中的协变和逆变是处理泛型类型兼容性的重要概念。数组的协变虽然方便,但可能会导致运行时异常。泛型的通配符(上界和下界)为我们提供了更安全和灵活的方式来实现协变和逆变。通过合理使用协变和逆变,可以提高代码的复用性和可维护性,同时保证类型安全。在实际开发中,根据方法是读取还是写入数据来选择合适的通配符。

参考资料

  • 《Effective Java》
  • Java 官方文档

通过以上内容,读者应该对 Java 中的协变和逆变有了更深入的理解,并能够在实际编程中高效使用它们。