我对Java泛型如何处理继承/多态性有点困惑。

假设以下层次结构-

动物(父母)

狗-猫(儿童)

所以假设我有一个doSomething方法(列出<Animal>动物)。根据继承和多态性的所有规则,我会假设List<Dog>是List<Animal>,List<Cat>是List<Animal>-因此任何一个都可以传递给这个方法。不是这样。如果我想实现这种行为,我必须通过说doSomething(list<?extendsAnimal>动物)来明确告诉方法接受Animal的任何子类的列表。

我知道这是Java的行为。我的问题是为什么?为什么多态性通常是隐式的,但当涉及泛型时,必须指定它?


当前回答

另一个解决方案是建立一个新的列表

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

其他回答

子类型对于参数化类型是不变的。即使严格来说,类Dog是Animal的子类型,但参数化类型List<Dog>不是List<Animal>的子类型。相反,协变子类型由数组使用,因此数组类型狗[]是动物[]的一个亚型。

不变的子类型确保不违反Java强制的类型约束。考虑@Jon Skeet给出的以下代码:

List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);

正如@Jon Skeet所说,这段代码是非法的,因为否则它会违反类型约束,在狗期望的时候返回一只猫。

将上述代码与数组的类似代码进行比较是有指导意义的。

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

该代码是合法的。但是,引发数组存储异常。数组在运行时携带其类型,JVM可以这样强制协变子类型的类型安全性。

为了进一步理解这一点,让我们看一下javap生成的字节码:

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

public class Demonstration {
    public void normal() {
        List normal = new ArrayList(1);
        normal.add("lorem ipsum");
    }

    public void parameterized() {
        List<String> parameterized = new ArrayList<>(1);
        parameterized.add("lorem ipsum");
    }
}

使用命令javap-c演示,这将显示以下Java字节码:

Compiled from "Demonstration.java"
public class Demonstration {
  public Demonstration();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void normal();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return

  public void parameterized();
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: iconst_1
       5: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V
       8: astore_1
       9: aload_1
      10: ldc           #4                  // String lorem ipsum
      12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      17: pop
      18: return
}

观察方法体的翻译代码是否相同。编译器通过删除来替换每个参数化类型。此属性至关重要,这意味着它不会破坏向后兼容性。

总之,参数化类型的运行时安全性是不可能的,因为编译器通过删除来替换每个参数化类型。这使得参数化类型只不过是语法糖。

List<Dog>不是List<Animal>的原因是,例如,您可以将猫插入List<Animate>,但不能插入List<Dog>。。。在可能的情况下,可以使用通配符使泛型更具可扩展性;例如,从List<Dog>中读取与从List<Animal>中读取类似,但不是写入。

《Java语言中的泛型》和《Java教程》中的“泛型”一节对为什么某些事物是多态的或不多态的或允许使用泛型进行了非常好、深入的解释。

这种行为的基本逻辑是泛型遵循类型擦除机制。因此,在运行时,您无法识别集合的类型,而不像阵列中没有这样的擦除过程。回到你的问题。。。

因此,假设有如下方法:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

现在,若java允许调用者将List of type Animal添加到此方法中,那个么您可能会将错误的内容添加到集合中,并且在运行时它也会由于类型擦除而运行。而在数组的情况下,您将获得此类场景的运行时异常。。。

因此,本质上,这种行为的实现是为了避免将错误的东西添加到集合中。现在我相信类型删除的存在是为了与没有泛型的遗留java兼容。。。。

让我们以JavaSE教程为例

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

因此,为什么狗(圆圈)的列表不应被视为动物(形状)的列表,是因为这种情况:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

因此,Java“架构师”有两个选项可以解决这个问题:

不要认为子类型是隐式的,它是父类型,并给出编译错误,就像现在发生的那样将子类型视为它的父类型,并在编译“add”方法时进行限制(因此在drawAll方法中,如果要传递圆的列表(形状的子类型),编译器应该检测到这一点,并用编译错误限制您这样做)。

出于明显的原因,他们选择了第一条路。

关于Jon Skeet的回答,他使用了以下示例代码:

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?

在最深层次上,这里的问题是狗和动物共享一个参考。这意味着一种方法是复制整个列表,这将打破引用相等:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

在调用List<Animal>animals=new ArrayList<>(狗);后;,您随后不能将动物直接分配给狗或猫:

// These are both illegal
dogs = animals;
cats = animals;

因此,您不能将错误的Animal子类型放入列表中,因为没有错误的子类型--任何子类型的对象?扩展动物可以添加到动物。

显然,这改变了语义,因为动物和狗的列表不再共享,所以添加到一个列表不会添加到另一个列表中(这正是您想要的,以避免将猫添加到只应包含狗对象的列表中的问题)。此外,复制整个列表可能效率低下。然而,通过打破引用相等性,这确实解决了类型等价问题。