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

假设以下层次结构-

动物(父母)

狗-猫(儿童)

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

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


当前回答

这里给出的答案并不能完全说服我,所以我再举一个例子。

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

听起来不错,不是吗?但你只能通过动物消费者和供应商。如果你有哺乳动物的消费者,但鸭子的供应商,尽管它们都是动物,但它们不应该适合。为了禁止这种行为,增加了额外的限制。

我们必须定义我们使用的类型之间的关系,而不是上述内容。

例如。,

public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

确保我们只能使用为消费者提供正确类型对象的供应商。

OTOH,我们也可以

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
    consumer.accept(supplier.get());
}

我们的方向相反:我们定义供应商的类型,并限制其可以投放给消费者。

我们甚至可以做到

public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
    consumer.accept(supplier.get());
}

其中,有了“生活”->“动物”->“哺乳动物”->“狗”、“猫”等直观关系,我们甚至可以将“哺乳动物”放入“生活”消费者,但不能将“字符串”放入“生命”消费者。

其他回答

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

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

问题已正确识别为与差异有关,但详细信息不正确。纯函数列表是协变数据函子,这意味着如果Sub类型是Super的子类型,那么Sub列表绝对是Super列表的子类型。

然而,列表的可变性并不是这里的基本问题。问题是总体上的可变性。这个问题是众所周知的,被称为协方差问题,我认为它是卡斯塔尼亚首先发现的,它完全彻底地破坏了作为一个通用范式的对象定向。这是基于Cardelli和Reynolds之前建立的方差规则。

有点过于简单化,让我们将T型对象B分配给T型对象A作为突变。这不失一般性:a的突变可以写成a=f(a),其中f:T->T。当然,问题是,虽然函数在其共域中是协变的,但它们在其域中是逆变的,但通过赋值,域和共域是相同的,因此赋值是不变的!

因此,概括而言,亚型不能突变。但是对象定向突变是根本的,因此对象定向本质上是有缺陷的。

这里有一个简单的例子:在纯函数设置中,对称矩阵显然是一个矩阵,它是一个子类型,没有问题。现在,让我们在矩阵中添加一项功能,即在坐标(x,y)处设置一个元素,规则是其他元素不变。现在对称矩阵不再是一个子类型,如果你改变了(x,y),你也改变了(y,x)。函数运算是delta:Sym->Mat,如果你改变对称矩阵的一个元素,你会得到一个一般的非对称矩阵。因此,如果在Mat中包含“更改一个元素”方法,Sym不是子类型。事实上几乎肯定没有合适的亚型。

简单地说,如果你有一个通用的数据类型,其中包含大量的变异器,这些变异器利用了它的通用性,你可以确定任何适当的子类型都不可能支持所有这些变异:如果可以,它将与超类型一样通用,与“适当”子类型的规范相反。

事实上,Java阻止了可变列表的子类型化,这并不能解决真正的问题:几十年前,当Java受到质疑时,为什么要使用面向对象的垃圾呢??

无论如何,这里有一个合理的讨论:

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(计算机科学)

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

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

这也是事实

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

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

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

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

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

因此,假设有如下方法:

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中时(例如),后者是当需要以类型安全的方式从中读取时。但同时做到这两个的唯一方法是拥有一种特定的类型。