![](/img/trans.png)
[英]Dog is Animal but list<Dog> is not list<Animal>. How to use it safely in a generic/polymorphic function?
[英]Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?
我对 Java generics 如何处理 inheritance / 多态性有点困惑。
假设以下层次结构 -
动物(父母)
狗-猫(儿童)
所以假设我有一个方法doSomething(List<Animal> animals)
。 根据 inheritance 和多态性的所有规则,我假设List<Dog>
是List<Animal>
并且List<Cat>
是List<Animal>
- 因此任何一个都可以传递给此方法。 不是这样。 如果我想实现这种行为,我必须通过说doSomething(List<? extends Animal> animals)
明确告诉该方法接受 Animal 的任何子类的列表。
我明白这是 Java 的行为。 我的问题是为什么? 为什么多态一般是隐式的,但是到了generics就必须指定?
不, List<Dog>
不是List<Animal>
。 考虑一下您可以用List<Animal>
做什么——您可以向其中添加任何动物……包括猫。 现在,你能合乎逻辑地将一只猫添加到一窝小狗中吗? 绝对不。
// 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?
突然你有一只非常困惑的猫。
现在,您不能将Cat
添加到List<? extends Animal>
List<? extends Animal>
因为你不知道它是一个List<Cat>
。 您可以检索一个值并知道它将是一个Animal
,但您不能添加任意动物。 List<? super Animal>
List<? super Animal>
- 在这种情况下,您可以安全地向其添加一个Animal
,但您不知道可以从中检索到什么,因为它可能是一个List<Object>
。
您正在寻找的是covariant type parameters 。 这意味着如果 object 的一种类型可以在方法中替换为另一种类型(例如, Animal
可以替换为Dog
),这同样适用于使用这些对象的表达式(因此List<Animal>
可以替换为List<Dog>
). 问题是协变对于一般的可变列表来说是不安全的。 假设你有一个List<Dog>
,它被用作List<Animal>
。 当您尝试将 Cat 添加到这个实际上是List<Dog>
的List<Animal>
时会发生什么? 自动允许类型参数协变会破坏类型系统。
添加语法以允许将类型参数指定为协变会很有用,这样可以避免? extends Foo
在方法声明中? extends Foo
,但这确实增加了额外的复杂性。
List<Dog>
不是List<Animal>
的原因是,例如,您可以将Cat
插入List<Animal>
,但不能插入List<Dog>
...您可以使用通配符来制作generics 在可能的情况下更具可扩展性; 例如,从List<Dog>
读取类似于从List<Animal>
读取——但不是写入。
Java 语言中的 Generics和Java 教程中关于 Generics的部分对 generics 为什么有些东西是或不是多态或允许的有很好、深入的解释。
List<Dog>
不是Java 中的List<Animal>
这也是事实
A list of dogs is-英文动物名录(合理解释下)
OP 直觉的工作方式——当然是完全有效的——是后一句话。 然而,如果我们应用这种直觉,我们会得到一种在其类型系统中不是 Java 风格的语言:假设我们的语言确实允许将一只猫添加到我们的狗列表中。 那是什么意思? 这意味着该列表不再是狗的列表,而仍然只是动物的列表。 以及哺乳动物列表和四足动物列表。
换句话说:Java中的A List<Dog>
在英文中并不是“a list of dogs”的意思,它的意思是“a list of dogs and nothing other than dogs”。
更一般地说, OP 的直觉适用于一种语言,在这种语言中,对对象的操作可以更改其类型,或者更确切地说,对象的类型是其值的(动态)function。
我会说 Generics 的全部意义在于它不允许这样做。 考虑 arrays 的情况,它确实允许这种类型的协方差:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
该代码编译正常,但会引发运行时错误(第二行中的java.lang.ArrayStoreException: java.lang.Boolean
)。 它不是类型安全的。 Generics 的要点是添加编译时类型安全,否则你可以坚持使用没有 generics 的普通 class。
现在有些时候您需要更加灵活,这就是? super Class
? super Class
和? extends Class
? extends Class
是为了。 前者是当您需要插入类型Collection
(例如)时,后者是当您需要以类型安全的方式从中读取时。 但同时进行这两项操作的唯一方法是拥有一个特定的类型。
为了理解这个问题,与 arrays 进行比较是很有用的。
List<Dog>
不是List<Animal>
的子类。
但是Dog[]
是Animal[]
的子类。
Arrays是具体化和协变的。
Reifiable意味着它们的类型信息在运行时是完全可用的。
因此 arrays 提供运行时类型安全但不提供编译时类型安全。
// 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
generics 反之亦然:
Generics 被擦除且不变。
因此 generics 不能提供运行时类型安全,但它们提供编译时类型安全。
在下面的代码中,如果 generics 是协变的,则可能会在第 3 行造成堆污染。
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
这里给出的答案并没有完全说服我。 因此,我举了另一个例子。
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
听起来不错,不是吗? 但是您只能为Animal
传递Consumer
和Supplier
。 如果您有一个Mammal
消费者,但是一个Duck
供应商,尽管它们都是动物,但它们不应该适合。 为了禁止这种情况,添加了额外的限制。
而不是上面的,我们必须定义我们使用的类型之间的关系。
例如,
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
确保我们只能使用为消费者提供正确类型 object 的供应商。
OTOH,我们也可以这样做
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
我们 go 另一种方式:我们定义Supplier
的类型并限制它可以放入Consumer
。
我们甚至可以做到
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
其中,具有Life
-> Animal
-> Mammal
-> Dog
, Cat
等直观关系,我们甚至可以将Mammal
放入Life
消费者中,但不能将String
放入Life
消费者中。
这种行为的基本逻辑是Generics
遵循类型擦除机制。 因此,在运行时,您无法识别不像arrays
那样没有此类擦除过程的collection
类型。 所以回到你的问题......
所以假设有一个方法如下:
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
现在,如果 java 允许调用者将 Animal 类型的列表添加到此方法中,那么您可能会将错误的东西添加到集合中,并且在运行时它也会由于类型擦除而运行。 而在 arrays 的情况下,您将获得此类场景的运行时异常......
因此,从本质上讲,这种行为的实现是为了防止将错误的东西添加到集合中。 现在我相信存在类型擦除,以便在没有 generics 的情况下与遗留 java 兼容....
其他人已经很好地解释了为什么不能将后代列表转换为超类列表。
但是,许多人访问此问题以寻找解决方案。
所以,从Java版本10开始解决这个问题如下:
(注意:S = 超类)
List<S> supers = List.copyOf( descendants );
如果绝对安全,这个 function 将执行强制转换,如果强制转换不安全,则执行复制。
如需深入解释(考虑到此处其他答案提到的潜在陷阱),请参阅相关问题和我 2022 年的回答: https://stackoverflow.com/a/72195980/773113
子类型对于参数化类型是不变的。 即使 class Dog
是Animal
的子类型,参数化类型List<Dog>
也不是List<Animal>
的子类型。 相反,arrays 使用协变子类型,因此数组类型Dog[]
是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 所述,此代码是非法的,因为否则它会在狗预期时返回猫,从而违反类型约束。
将上面的代码与 arrays 的类似代码进行比较是有益的。
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
该代码是合法的。 但是,抛出数组存储异常。 数组在运行时以这种方式携带其类型 JVM 可以强制协变子类型化的类型安全。
为了进一步理解这一点,让我们看看下面 class 的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 Demonstration
,这显示了以下 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
}
观察方法体的翻译代码是相同的。 编译器用擦除替换了每个参数化类型。 此属性至关重要,意味着它不会破坏向后兼容性。
总之,运行时安全对于参数化类型是不可能的,因为编译器通过擦除来替换每个参数化类型。 这使得参数化类型只不过是语法糖。
实际上你可以使用一个接口来实现你想要的。
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
然后您可以使用集合使用
List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
如果您确定列表项是给定超类型的子类,则可以使用以下方法转换列表:
(List<Animal>) (List<?>) dogs
当你想在构造函数中传递列表或迭代它时,这很有用。
答案以及其他答案都是正确的。 我将使用我认为有用的解决方案添加到这些答案中。 我认为这在编程中经常出现。 需要注意的一件事是,对于 Collections(列表、集合等),主要问题是添加到集合中。 这就是事情崩溃的地方。 即使删除也可以。
在大多数情况下,我们可以使用Collection<? extends T>
Collection<? extends T>
而不是Collection<T>
,这应该是首选。 但是,我发现在某些情况下不容易做到这一点。 关于这是否总是最好的做法还有待讨论。 我在这里展示一个 class DownCastCollection,它可以转换一个Collection<? extends T>
Collection<? extends T>
到Collection<T>
(我们可以为 List、Set、NavigableSet 等定义类似的类),在使用标准方法时非常不方便。 下面是一个如何使用它的示例(在这种情况下我们也可以使用Collection<? extends Object>
,但我尽量简单地说明如何使用 DownCastCollection。
/**Could use Collection<? extends Object> and that is the better choice.
* But I am doing this to illustrate how to use DownCastCollection. **/
public static void print(Collection<Object> col){
for(Object obj : col){
System.out.println(obj);
}
}
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c"));
print(new DownCastCollection<Object>(list));
}
现在是 class:
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;
public DownCastCollection(Collection<? extends E> delegate) {
super();
this.delegate = delegate;
}
@Override
public int size() {
return delegate ==null ? 0 : delegate.size();
}
@Override
public boolean isEmpty() {
return delegate==null || delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
if(isEmpty()) return false;
return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
Iterator<? extends E> delegateIterator;
protected MyIterator() {
super();
this.delegateIterator = delegate == null ? null :delegate.iterator();
}
@Override
public boolean hasNext() {
return delegateIterator != null && delegateIterator.hasNext();
}
@Override
public E next() {
if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
return delegateIterator.next();
}
@Override
public void remove() {
delegateIterator.remove();
}
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if(delegate == null) return false;
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
if(delegate==null) return false;
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.retainAll(c);
}
@Override
public void clear() {
if(delegate == null) return;
delegate.clear();
}
}
该问题已被正确识别为与差异相关,但详细信息不正确。 纯函数列表是协变数据函子,这意味着如果类型 Sub 是 Super 的子类型,则 Sub 列表肯定是 Super 列表的子类型。
然而,列表的可变性并不是这里的基本问题。 问题是一般的可变性。 这个问题众所周知,被称为协方差问题,我认为它首先是由 Castagna 发现的,它彻底彻底地破坏了 object 作为一般范式的方向。 它基于先前由 Cardelli 和 Reynolds 建立的方差规则。
有点过分简化,让我们考虑将类型 T 的 object B 分配给类型 T 的 object A 作为突变。 这不失一般性:A 的一个突变可以写成 A = f (A) 其中 f: T -> T。当然,问题是虽然函数在它们的余域中是协变的,但它们在它们的域中是逆变的域,但是对于赋值,域和辅域是相同的,所以赋值是不变的!
概括地说,子类型不能突变。 但是 object 方向突变是基本的,因此 object 方向本质上是有缺陷的。
这是一个简单的例子:在纯函数设置中,对称矩阵显然是一个矩阵,它是一个子类型,没问题。 现在让我们向矩阵添加在坐标 (x,y) 处设置单个元素的功能,并遵循其他元素不变的规则。 现在对称矩阵不再是子类型,如果你改变 (x,y) 你也改变了 (y,x)。 函数运算是 delta:Sym -> Mat,如果你改变一个对称矩阵的一个元素,你会得到一个一般的非对称矩阵。 因此,如果您在 Mat 中包含“更改一个元素”方法,则 Sym 不是子类型。 事实上..几乎可以肯定没有合适的子类型。
用更简单的术语来说:如果你有一个通用数据类型,具有广泛的变异器,这些变异器利用了它的通用性,你可以确定任何适当的子类型都不可能支持所有这些变异:如果可以的话,它将与超类型,与“适当的”子类型的规范相反。
Java 阻止子类型化可变列表的事实未能解决真正的问题:为什么你使用面向 object 的垃圾,比如 Java 几十年前它就被抹黑了?
无论如何,这里有一个合理的讨论:
https://en.wikipedia.org/wiki/Covariance_and_contravariance_(计算机科学)
让我们以 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“建筑师”有 2 个选项可以解决这个问题:
不要认为子类型是隐含的超类型,并给出编译错误,就像现在发生的那样
将子类型视为它的超类型并在编译“添加”方法时进行限制(因此在 drawAll 方法中,如果将传递圆列表、形状的子类型,编译器应该检测到并限制你执行编译错误那)。
出于显而易见的原因,它选择了第一种方式。
我们还应该考虑编译器如何威胁泛型类:每当我们填充泛型 arguments 时“实例化”一个不同的类型。
因此,我们有ListOfAnimal
、 ListOfDog
、 ListOfCat
等,它们是不同的类,当我们指定泛型 arguments 时,它们最终由编译器“创建”。这是一个平面层次结构(实际上关于List
根本不是层次结构) .
协变在泛型类的情况下没有意义的另一个论点是所有类在基础上都是相同的——都是List
实例。 通过填充通用参数来专门化List
不会扩展 class,它只是让它适用于该特定的通用参数。
问题已经很清楚了。 但是有一个解决方案; make doSomething通用的:
<T extends Animal> void doSomething<List<T> animals) {
}
现在您可以使用 List<Dog> 或 List<Cat> 或 List<Animal> 调用 doSomething。
另一种解决方案是建立一个新列表
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
进一步 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?
在最深层次上,这里的问题是dogs
和animals
共享一个参考。 这意味着完成这项工作的一种方法是复制整个列表,这会破坏引用相等性:
// 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<>(dogs);
,您不能随后直接将animals
分配dogs
或cats
:
// These are both illegal
dogs = animals;
cats = animals;
因此,您不能将错误的Animal
子类型放入列表中,因为没有错误的子类型——任何 object 子类型? extends Animal
? extends Animal
可以添加到animals
。
显然,这改变了语义,因为列表animals
和dogs
不再共享,所以添加到一个列表不会添加到另一个列表(这正是你想要的,以避免将Cat
添加到列表中的问题应该只包含Dog
对象)。 此外,复制整个列表可能效率低下。 但是,这确实通过打破引用相等性解决了类型等价问题。
我看到这个问题已经回答了很多次,只是想就同一个问题发表我的意见。
让我们 go 向前创建一个简化的 Animal class 层次结构。
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());
}
}
}
class 编译正常,当我们运行上面的 class 时,我们得到 output
animal eating
animal eating
animal eating
animal eating
animal eating
animal eating
这里要注意的一点是,takeAnimals() 方法被定义为接受 Animal 类型的任何东西,它可以接受 Animal 类型的数组,也可以接受 Dog 的数组,因为 Dog-is-a-Animal。 所以这就是多态性在起作用。
现在让我们对 generics 使用同样的方法,
现在假设我们稍微调整一下代码并使用 ArrayLists 而不是 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());
}
}
}
上面的 class 将编译并生成 output -
animal eating
animal eating
animal eating
animal eating
animal eating
animal eating
所以我们知道这是有效的,现在让我们稍微调整一下这个 class 以多态地使用 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());
}
}
}
看起来编译上面的 class 应该没有问题,因为 takeAnimals() 方法被设计为接受 Animal 和 Dog-is-a-Animal 类型的任何 ArrayList,所以它在这里不应该是一个交易破坏者。
但是,不幸的是,编译器抛出一个错误,不允许我们将 Dog ArrayList 传递给需要 Animal ArrayList 的变量。
你问为什么?
因为试想一下,如果 JAVA 允许 Dog ArrayList - dogs - 被放入 Animal ArrayList - animals - 然后在 takeAnimals() 方法中有人做了类似的事情 -
animals.add(new Cat());
认为这应该是可行的,因为理想情况下它是一个动物 ArrayList,你应该在 position 中添加任何猫作为猫也是动物,但实际上你传递了一个狗类型 ArrayList 给它。
所以,现在你一定在想同样的事情也应该发生在 Arrays 上。 你这样想是对的。
如果有人试图用 Arrays 做同样的事情,那么 Arrays 也会抛出一个错误,但是 Arrays 在运行时处理这个错误,而 ArrayLists 在编译时处理这个错误。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.