我对Java泛型如何处理继承/多态性有点困惑。
假设以下层次结构-
动物(父母)
狗-猫(儿童)
所以假设我有一个doSomething方法(列出<Animal>动物)。根据继承和多态性的所有规则,我会假设List<Dog>是List<Animal>,List<Cat>是List<Animal>-因此任何一个都可以传递给这个方法。不是这样。如果我想实现这种行为,我必须通过说doSomething(list<?extendsAnimal>动物)来明确告诉方法接受Animal的任何子类的列表。
我知道这是Java的行为。我的问题是为什么?为什么多态性通常是隐式的,但当涉及泛型时,必须指定它?
问题已正确识别为与差异有关,但详细信息不正确。纯函数列表是协变数据函子,这意味着如果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_(计算机科学)
不,列表<狗>不是列表<动物>。考虑一下你可以用列表<动物>做什么——你可以在其中添加任何动物……包括一只猫。现在,你能在一窝小狗中加入一只猫吗?绝对不是。
// 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?
突然你有一只非常困惑的猫。
现在,您不能将猫添加到列表<?扩展Animal>,因为你不知道它是List<Cat>。您可以检索一个值并知道它将是一个Animal,但不能添加任意的Animal。列表<?super Animal>-在这种情况下,您可以安全地将Animal添加到其中,但您不知道可能从中检索到什么,因为它可能是List<Object>。
这种行为的基本逻辑是泛型遵循类型擦除机制。因此,在运行时,您无法识别集合的类型,而不像阵列中没有这样的擦除过程。回到你的问题。。。
因此,假设有如下方法:
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兼容。。。。
问题已正确识别为与差异有关,但详细信息不正确。纯函数列表是协变数据函子,这意味着如果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_(计算机科学)
关于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子类型放入列表中,因为没有错误的子类型--任何子类型的对象?扩展动物可以添加到动物。
显然,这改变了语义,因为动物和狗的列表不再共享,所以添加到一个列表不会添加到另一个列表中(这正是您想要的,以避免将猫添加到只应包含狗对象的列表中的问题)。此外,复制整个列表可能效率低下。然而,通过打破引用相等性,这确实解决了类型等价问题。