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

假设以下层次结构-

动物(父母)

狗-猫(儿童)

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

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


当前回答

如果您确定列表项是给定超类型的子类,则可以使用以下方法强制转换列表:

(List<Animal>) (List<?>) dogs

当您想要在构造函数内部传递列表或对其进行迭代时,这是非常有用的。

其他回答

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

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

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

让我们继续创建一个简化的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在编译时处理这个问题。

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

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

您要查找的是所谓的协变类型参数。这意味着,如果在方法中可以用一种类型的对象替换另一种类型(例如,Animal可以替换为Dog),那么同样的情况也适用于使用这些对象的表达式(因此List<Animal>可以替换为List<Dog>)。问题是,一般来说,协方差对于可变列表来说是不安全的。假设您有一个List<Dog>,并且它被用作List<Animal>。当你试图将一只猫添加到这个列表<动物>中时会发生什么?自动允许类型参数为协变会破坏类型系统。

添加语法以允许将类型参数指定为协变将是有用的,这避免了?在方法声明中扩展了Foo,但这确实增加了额外的复杂性。

我认为,其他答案中提到的一点是

List<Dog>不是Java中的List<Animal>

这也是事实

狗的列表是英文的动物列表(在合理的解释下)

OP的直觉运作方式——当然这是完全有效的——是后一句话。然而,如果我们运用这种直觉,我们得到的语言在其类型系统中不是Java风格的:假设我们的语言确实允许将猫添加到我们的狗列表中。这意味着什么?这意味着该列表不再是狗的列表,而仅仅是动物的列表。还有一张哺乳动物的列表和一张四足动物的列表。

换一种说法:Java中的List<Dog>在英语中并不意味着“一个狗的列表”,它的意思是“一个只有狗的列表。”。

更一般地说,OP的直觉倾向于一种语言,在这种语言中,对对象的操作可以改变其类型,或者更确切地说,对象的类型是其值的(动态)函数。