返回

泛型与隐式多态性的迷局:`List<Dog>`为何不是`List<Animal>`的子类?

java

泛型与隐式多态性:揭秘 List 是否是 List 的子类

简介

泛型和隐式多态性是 Java 语言中两大重要的特性,它们可以提升代码的灵活性和可扩展性。然而,这两个概念之间的微妙差异可能会让人困惑。本文将深入探究泛型与隐式多态性的关系,并揭秘为什么 List<Dog> 不是 List<Animal> 的子类。

隐式多态性

多态性允许一个对象以其父类或接口类型进行交互。也就是说,我们可以将一个子类的实例分配给父类或接口类型的变量,并且该变量仍能正确地调用子类的方法。

Animal animal = new Dog();
animal.makeSound(); // 输出 "汪汪"

在上面的示例中,我们创建了一个 Dog 对象并将其分配给 Animal 类型的变量 animal。当我们调用 makeSound() 方法时,会执行 Dog 类中重写的版本,输出 "汪汪"。

泛型的复杂性

泛型允许我们创建类型化的集合,其中元素可以是任何类型。例如,我们可以创建一个 List<Animal> 来存储 Animal 类型的对象。

List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());

当我们尝试将 List<Dog>List<Cat> 赋值给 List<Animal> 时,问题就出现了。

List<Animal> animals = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
animals = dogs; // 编译错误

编译器会报错,因为 List<Dog> 不是 List<Animal> 的子类。这与我们对多态性的直观理解相悖,因为它表明 List<Dog> 应该可以隐式转换为 List<Animal>

泛型与隐式多态性的差异

为什么泛型不能隐式地转换为其父类或接口类型呢?这是因为泛型类型参数是类型擦除的。当编译器编译泛型代码时,它会删除类型参数,并用实际类型替换它们。

List<Animal> animals = new ArrayList<Animal>();
// 编译后变为:
List animals = new ArrayList();

这意味着 List<Dog> 在编译后实际上是一个 List,它可以存储任何类型的对象,而不仅仅是 Animal。这就是为什么它不能隐式转换为 List<Animal> 的原因。

通配符的救星

为了让泛型和多态性协同工作,Java 引入了通配符。通配符允许我们指定一个类型变量可以是任何类型或其子类型。

List<? extends Animal> animals = new ArrayList<>();

在这个示例中,? extends Animal 通配符表示 animals 列表可以存储任何 Animal 的子类型,包括 DogCat。现在,我们可以将 List<Dog>List<Cat> 赋值给 animals 列表,因为它可以隐式转换为 List<? extends Animal>

结论

泛型和隐式多态性是 Java 语言中强大的特性,但它们的行为却有所不同。泛型类型参数是类型擦除的,因此它们不能隐式转换为其父类或接口类型。为了解决这个问题,我们可以使用通配符来指定泛型类型参数可以是任何类型或其子类型,从而实现泛型和多态性的协同工作。

常见问题解答

  1. 为什么 List<Dog> 不是 List<Animal> 的子类?
    答:因为泛型类型参数是类型擦除的,编译后 List<Dog> 实际上是一个 List,它可以存储任何类型的对象。

  2. 如何让 List<Dog> 可以隐式转换为 List<Animal>
    答:使用通配符 ? extends Animal,它表示列表可以存储任何 Animal 的子类型。

  3. 什么是类型擦除?
    答:编译器在编译泛型代码时,会删除类型参数,并用实际类型替换它们。

  4. 通配符是如何工作的?
    答:通配符允许指定一个类型变量可以是任何类型或其子类型,从而允许泛型和多态性协同工作。

  5. 什么时候应该使用泛型和通配符?
    答:当我们需要创建类型化的集合或指定一个类型变量可以是任何类型或其子类型时,可以使用泛型和通配符。