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

假设以下层次结构-

动物(父母)

狗-猫(儿童)

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

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


当前回答

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

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

其他回答

我看到这个问题已经被回答了很多次,只想在同一个问题上输入我的意见。

让我们继续创建一个简化的Animal类层次结构。

abstract class Animal {
    void eat() {
        System.out.println("animal eating");
    }
}

class Dog extends Animal {
    void bark() { }
}

class Cat extends Animal {
    void meow() { }
}

现在让我们看看我们的老朋友Arrays,我们知道它隐式支持多态性-

class TestAnimals {
    public static void main(String[] args) {
        Animal[] animals = {new Dog(), new Cat(), new Dog()};
        Dog[] dogs = {new Dog(), new Dog(), new Dog()};
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(Animal[] animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

该类编译良好,当我们运行上面的类时,我们得到输出

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

这里需要注意的是,takeAnimals()方法被定义为接受Animal类型的任何东西,它可以接受Animal类型的数组,也可以接受Dog类型的数组。这就是多态性的作用。

现在让我们对泛型使用相同的方法,

现在假设我们稍微调整一下代码,使用ArrayList而不是Arrays-

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());
        takeAnimals(animals);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

上面的类将编译并生成输出-

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

所以我们知道这是可行的,现在让我们稍微调整一下这个类,使其以多态的方式使用Animal类型-

class TestAnimals {
    public static void main(String[] args) {
        ArrayList<Animal> animals = new ArrayList<Animal>();
        animals.add(new Dog());
        animals.add(new Cat());
        animals.add(new Dog());

        ArrayList<Dog> dogs = new ArrayList<Dog>();
        takeAnimals(animals);
        takeAnimals(dogs);
    }

    public void takeAnimals(ArrayList<Animal> animals) {
        for(Animal a : animals) {
            System.out.println(a.eat());
        }
    }   
}

看起来编译上面的类应该没有问题,因为takeAnimals()方法被设计为接受Animal和Dog-is-Animal类型的任何ArrayList,因此它不应该成为交易破坏者。

但是,不幸的是,编译器抛出了一个错误,不允许我们将Dog ArrayList传递给期望Animal ArrayList的变量。

你问为什么?

因为想象一下,如果JAVA允许将Dog ArrayList-dogs-放入Animal ArrayList中-animals-然后在takeAnimals()方法中,有人会这样做-

animals.add(new Cat());

认为这应该是可行的,因为理想情况下它是一个Animal ArrayList,您应该能够将任何猫添加到其中,作为cat-is-also-Animal,但实际上您将一个Dog类型的ArrayList传递给了它。

所以,现在您必须想到,数组也应该发生同样的情况。你这样想是对的。

如果有人试图用Arrays做同样的事情,那么Arrays也会抛出一个错误,但Arrays在运行时处理这个错误,而ArrayList在编译时处理这个问题。

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

因此,假设有如下方法:

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兼容。。。。

我想说,Generics的全部观点是它不允许这样做。考虑数组的情况,它确实允许这种类型的协方差:

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

该代码编译良好,但抛出运行时错误(第二行中的java.lang.ArrayStoreException:java.lang.Boolean)。它不是类型安全的。Generics的目的是增加编译时类型安全性,否则您可以只使用没有泛型的普通类。

现在有些时候你需要更加灵活,这就是为什么?超级班和?扩展类用于。前者是当您需要插入到类型Collection中时(例如),后者是当需要以类型安全的方式从中读取时。但同时做到这两个的唯一方法是拥有一种特定的类型。

关于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子类型放入列表中,因为没有错误的子类型--任何子类型的对象?扩展动物可以添加到动物。

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

为了理解这个问题,比较数组是很有用的。

List<Dog>不是List<Animal>的子类。但狗[]是动物[]的子类。

数组是可具体化和协变的。可重用意味着它们的类型信息在运行时完全可用。因此,数组提供运行时类型安全性,但不提供编译时类型安全。

    // All compiles but throws ArrayStoreException at runtime at last line
    Dog[] dogs = new Dog[10];
    Animal[] animals = dogs; // compiles
    animals[0] = new Cat(); // throws ArrayStoreException at runtime

对于泛型,情况也是如此:泛型被删除且不变。因此,泛型不能提供运行时类型安全,但它们提供编译时类型安全。在下面的代码中,如果泛型是协变的,则可能在第3行造成堆污染。

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());