列表<狗>是列表<动物>的子类吗?为什么Java泛型不是隐式多态的?

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

假设以下层次结构-

动物(家长)

-Cat(儿童)

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

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

132583 次浏览

不,List<Dog>没有List<Animal>。考虑一下你可以用List<Animal>做什么——你可以在里面添加任何动物……包括一只猫。现在,你能在一窝小狗上添加一只猫吗?绝对不行。

// Illegal code - because otherwise life would be BadList<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements ListList<Animal> animals = dogs; // Awooga awoogaanimals.add(new Cat());Dog dog = dogs.get(0); // This should be safe, right?

突然你有一只非常困惑的猫。

现在,您可以不能Cat添加到List<? extends Animal>,因为您不知道它是List<Cat>。您可以检索一个值并知道它将是Animal,但您不能添加任意动物。List<? super Animal>则相反-在这种情况下,您可以安全地将Animal添加到它,但您不知道可能从中检索到什么,因为它可能是List<Object>

你要找的叫做协变类型参数。这意味着如果一种类型的对象可以在方法中替换另一种类型的对象(例如,Animal可以替换为Dog),同样的情况也适用于使用这些对象的表达式(所以List<Animal>可以替换为List<Dog>)。问题是协方差通常对可变列表来说是不安全的。假设你有一个List<Dog>,它被用作List<Animal>。当你试图向这个List<Animal>添加一个Cat时,会发生什么,这实际上是一个List<Dog>?自动允许类型参数为协变打破了类型系统。

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

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

Java语言中的泛型Java教程中的泛型部分有一个非常好的,深入的解释,为什么有些东西是多态的或不允许泛型。

我会说泛型的全部意义在于它不允许这样做。考虑数组的情况,它确实允许这种类型的协方差:

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

这段代码编译得很好,但会抛出一个运行时错误(第二行中的java.lang.ArrayStoreException: java.lang.Boolean)。它不是类型安全的。泛型的重点是添加编译时类型安全,否则你可以坚持使用没有泛型的普通类。

现在有些时候你需要更加灵活,这就是? super Class? extends Class的作用。前者是当你需要插入类型Collection(例如)时,后者是当你需要以类型安全的方式从中读取时。但同时做到这两者的唯一方法是拥有一个特定的类型。

这种行为的基本逻辑是Generics遵循类型擦除机制。因此,在运行时,你无法识别collection的类型,而arrays没有这种擦除过程。所以回到你的问题…

所以假设有下面给出的方法:

add(List<Animal>){//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism}

现在,如果java允许调用者将动物类型的List添加到此方法,那么您可能会将错误的东西添加到集合中,并且在运行时也会由于类型擦除而运行。而在数组的情况下,您将获得此类场景的运行时异常…

因此,从本质上讲,这种行为的实现是为了不向集合中添加错误的东西。现在我相信类型擦除的存在是为了与没有泛型的传统java兼容……

我认为应该添加到其他答案提到的一点是,虽然

List<Dog>不是List<Animal>在Java

这也是事实

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

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

换句话说:Java中的AList<Dog>在英语中并不意味着“狗的列表”,它的意思是“狗的列表,只有狗”。

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

回答和其他答案都是正确的。我将用一个我认为会有帮助的解决方案来添加到这些答案中。我认为这在编程中经常出现。需要注意的一件事是,对于集合(列表、集合等),主要问题是添加到集合中。这就是事情崩溃的地方。甚至删除也可以。

在大多数情况下,我们可以使用Collection<? extends T>而不是Collection<T>,这应该是首选。然而,我发现这样做并不容易的情况。关于这是否总是最好的做法,还有待讨论。我在这里展示了一个类DownCastCollection,它可以将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));}

现在类:

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;}
@Overridepublic int size() {return delegate ==null ? 0 : delegate.size();}
@Overridepublic boolean isEmpty() {return delegate==null || delegate.isEmpty();}
@Overridepublic 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();}
@Overridepublic boolean hasNext() {return delegateIterator != null && delegateIterator.hasNext();}
@Overridepublic  E next() {if(!hasNext()) throw new NoSuchElementException("The iterator is empty");return delegateIterator.next();}
@Overridepublic void remove() {delegateIterator.remove();
}
}@Overridepublic Iterator<E> iterator() {return new MyIterator();}


@Overridepublic boolean add(E e) {throw new UnsupportedOperationException();}
@Overridepublic boolean remove(Object o) {if(delegate == null) return false;return delegate.remove(o);}
@Overridepublic boolean containsAll(Collection<?> c) {if(delegate==null) return false;return delegate.containsAll(c);}
@Overridepublic boolean addAll(Collection<? extends E> c) {throw new UnsupportedOperationException();}
@Overridepublic boolean removeAll(Collection<?> c) {if(delegate == null) return false;return delegate.removeAll(c);}
@Overridepublic boolean retainAll(Collection<?> c) {if(delegate == null) return false;return delegate.retainAll(c);}
@Overridepublic void clear() {if(delegate == null) return;delegate.clear();
}

}

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

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

听起来不错,不是吗?但是你只能通过ConsumerSupplier来获得Animal。如果你有一个Mammal的消费者,但一个Duck的供应商,他们不应该适合,尽管两者都是动物。为了不允许这一点,增加了额外的限制。

与上述不同,我们必须定义我们使用的类型之间的关系。

例如:

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

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

奥托,我们也可以

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

我们走另一条路:我们定义Supplier的类型并限制它可以放入Consumer

我们甚至可以做

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

其中,具有直观的关系Life->Animal->Mammal->DogCat等,我们甚至可以将Mammal放入Life消费者,但不能将String放入Life消费者。

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

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

当您想在构造函数内部传递列表或迭代它时,这很有用。

让我们以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 calldrawAll(circleList);

public void drawAll(List<Shape> shapes) {shapes.add(new Rectangle());}

所以Java“建筑师”有两个选择来解决这个问题:

  1. 不要认为一个子类型隐含地是它的超类型,并给出一个编译错误,就像现在发生的那样

  2. 考虑子类型是它的超类型,并在编译“add”方法时进行限制(因此在DradAll方法中,如果将传递圆形列表,形状的子类型,编译器应该检测到并限制您使用编译错误来执行此操作)。

出于显而易见的原因,他选择了第一种方式。

要理解这个问题,与数组进行比较很有用。

List<Dog>List<Animal>没有子类。
但是Dog[]Animal[]的子类。

数组是可实现和协变的.
可实现表示它们的类型信息在运行时完全可用。
因此数组提供运行时类型安全,但不提供编译时类型安全。

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

泛型反之亦然:

因此泛型不能提供运行时类型安全,但它们提供编译时类型安全。
在下面的代码中,如果泛型是协变的,则可以在第3行制作堆污染

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

我们还应该考虑编译器如何威胁泛型类:在“实例化”中,每当我们填充泛型参数时,都会使用不同的类型。

因此,我们有ListOfAnimalListOfDogListOfCat等,它们是不同的类,最终由编译器在我们指定泛型参数时“创建”。这是一个扁平的层次结构(实际上关于List根本不是层次结构)。

在泛型类的情况下协方差没有意义的另一个论点是,在基本上所有类都是相同的-是List实例。通过填充泛型参数来指定List不会扩展类,它只是使它适用于特定的泛型参数。

这个问题已经被很好地发现了。但是有一个解决方案;使做点什么泛化:

<T extends Animal> void doSomething<List<T> animals) {}

现在,您可以使用List或List或List<动物>调用doThings。

对于参数化类型,子类型是不变量。即使类DogAnimal的子类型,参数化类型List<Dog>也不是List<Animal>的子类型。相比之下,协变子类型被数组使用,因此数组类型Dog[]Animal[]的子类型。

不变子类型确保不违反Java强制执行的类型约束。考虑@Jon Skeet给出的以下代码:

List<Dog> dogs = new ArrayList<Dog>(1);List<Animal> animals = dogs;animals.add(new Cat()); // compile-time errorDog dog = dogs.get(0);

正如@Jon Skeet所述,此代码是非法的,因为否则它将违反类型约束,在狗期望时返回猫。

将上述代码与数组的类似代码进行比较是有益的。

Dog[] dogs = new Dog[1];Object[] animals = dogs;animals[0] = new Cat(); // run-time errorDog dog = dogs[0];

代码是合法的。但是,抛出一个数组存储异常。数组在运行时携带其类型,JVM可以通过这种方式强制执行协变子类型的类型安全性。

为了进一步理解这一点,让我们看看下面类的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_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: return
public void normal();Code:0: new           #2                  // class java/util/ArrayList3: dup4: iconst_15: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V8: astore_19: aload_110: ldc           #4                  // String lorem ipsum12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z17: pop18: return
public void parameterized();Code:0: new           #2                  // class java/util/ArrayList3: dup4: iconst_15: invokespecial #3                  // Method java/util/ArrayList."<init>":(I)V8: astore_19: aload_110: ldc           #4                  // String lorem ipsum12: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z17: pop18: return}

注意方法体的翻译代码是相同的。编译器用它的擦除替换每个参数化类型。这个属性至关重要,这意味着它没有破坏向后兼容性。

总之,参数化类型的运行时安全是不可能的,因为编译器会通过擦除来替换每个参数化类型。这使得参数化类型只不过是语法糖。

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

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 BadList<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements ListList<Animal> animals = dogs; // Awooga awoogaanimals.add(new Cat());Dog dog = dogs.get(0); // This should be safe, right?

在最深的层次上,这里的问题是dogsanimals共享一个引用。这意味着完成这项工作的一种方法是复制整个列表,这将打破引用相等:

// This code is fineList<Dog> dogs = new ArrayList<Dog>();dogs.add(new Dog());List<Animal> animals = new ArrayList<>(dogs); // Copy listanimals.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直接分配给dogscats

// These are both illegaldogs = animals;cats = animals;

因此,您不能将错误的子类型Animal放入列表中,因为没有错误的子类型-任何子类型? extends Animal的对象都可以添加到animals

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

该问题已被正确识别为与方差有关,但细节不正确。纯函数列表是协变数据仿函数,这意味着如果类型Sub是Super的子类型,那么Sub列表肯定是Super列表的子类型。

然而,列表的可变性不是这里的基本问题。问题是一般的可变性。这个问题是众所周知的,被称为协方差问题,我认为它首先是由Castagna确定的,它完全彻底地破坏了作为一般范式的对象定向。它基于先前由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_(computer_science)

我看到这个问题已经回答了很多次,只是想就同一个问题提出我的意见。

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

abstract class Animal {void eat() {System.out.println("animal eating");}}
class Dog extends Animal {void bark() { }}
class Cat extends Animal {void meow() { }}

现在让我们看看我们的老朋友数组,我们知道它隐含地支持多态性-

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 eatinganimal eatinganimal eatinganimal eatinganimal eatinganimal eating

这里需要注意的一点是,定义的方法是接受任何动物类型的东西,它可以接受一个动物类型的数组,也可以接受一个狗的数组,因为狗是动物。所以这是多态性在起作用。

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

现在假设我们稍微调整我们的代码并使用ArrayList而不是数组-

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 eatinganimal eatinganimal eatinganimal eatinganimal eatinganimal eating

所以我们知道这是有效的,现在让我们稍微调整这个类,使用动物类型多态-

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());}}}

看起来编译上面的类应该没有问题,因为bex动物()方法旨在获取任何类型为动物和狗是动物的ArrayList,所以它不应该成为这里的交易破坏者。

但是,不幸的是,编译器抛出了一个错误,不允许我们将狗数组列表传递给期望动物数组列表的变量。

你问为什么?

因为想象一下,如果JAVA允许DogArrayList(狗)被放入动物ArrayList(动物)中,然后在服用动物()方法中,有人做了类似的事情-

animals.add(new Cat());

认为这应该是可行的,因为理想情况下它是一个动物ArrayList,你应该能够将任何猫添加到它作为Cat-is-is-an-a-动物,但实际上你传递了一个狗类型的ArrayList给它。

所以,现在你一定认为同样的情况也应该发生在数组上。你这样想是对的。

如果有人试图对数组做同样的事情,那么数组也会抛出错误,但数组在运行时处理此错误,而数组列表在编译时处理此错误。

其他人已经做了一个体面的工作来解释为什么你不能仅仅将一个子类列表转换为超类列表。

然而,许多人访问这个问题寻求解决方案。

因此,自版本10以来Java这个问题的解决方案如下:

(注:S=超类)

List<S> supers = List.copyOf( descendants );

如果完全安全,此函数将执行强制转换,如果强制转换不安全,则执行副本。

有关深入解释(考虑到此处其他答案提到的潜在陷阱),请参阅相关问题和我2022年对它的回答:https://stackoverflow.com/a/72195980/773113