List<Dog> 是 List<Animal> 的子类吗?为啥 Java 泛型不是隐式多态的?

Posted

技术标签:

【中文标题】List<Dog> 是 List<Animal> 的子类吗?为啥 Java 泛型不是隐式多态的?【英文标题】:Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?List<Dog> 是 List<Animal> 的子类吗?为什么 Java 泛型不是隐式多态的? 【发布时间】:2022-01-19 21:09:21 【问题描述】:

我对 Java 泛型如何处理继承/多态感到有些困惑。

假设以下层次结构 -

动物(父母)

- (儿童)

所以假设我有一个方法doSomething(List&lt;Animal&gt; animals)。根据继承和多态的所有规则,我会假设 List&lt;Dog&gt; is List&lt;Animal&gt;List&lt;Cat&gt; is List&lt;Animal&gt; - 所以要么一个可以传递给这个方法。不是这样。如果我想实现这种行为,我必须通过说doSomething(List&lt;? extends Animal&gt; animals) 来明确告诉该方法接受 Animal 的任何子类的列表。

我了解这是 Java 的行为。我的问题是为什么?为什么多态性一般是隐含的,而泛型却必须指定?

【问题讨论】:

还有一个完全不相关的语法问题现在困扰着我——我的标题应该是“为什么 不是 Java 的泛型”还是“为什么 不是 Java 的泛型"?? “泛型”是因为 s 的复数还是单数因为它是一个实体? Java 中的泛型是一种非常糟糕的参数多态形式。不要太相信它们(就像我以前那样),因为有一天你会严重打击它们可悲的局限性:Surgeon extends Handable、Handable KABOOM! 计算[TM]。有你的Java泛型限制。任何 OOA/OOD 都可以很好地翻译成 Java(并且 MI 可以使用 Java 接口很好地完成),但泛型并不能解决它。它们适用于“集合”和程序化编程(这是大多数 Java 程序员无论如何都这样做的......)。 List 的超类不是 List 而是 List>(即未知类型的列表)。泛型擦除编译代码中的类型信息。这样做是为了使使用泛型(java 5 及以上)的代码与没有泛型的早期版本的 java 兼容。 相关的 SO 问题 - Whats the use of saying <? extends SomeObject> instead of <SomeObject> @froadie 因为似乎没有人回应......它肯定应该是“为什么不是 Java 的泛型......”。另一个问题是“generic”实际上是一个形容词,因此“generics”指的是由“generic”修饰的一个已删除的复数名词。你可以说“那个函数是通用的”,但这比说“那个函数是通用的”更麻烦。但是,说“Java 有泛型函数和类”,而不仅仅是“Java 有泛型”,这有点麻烦。作为一个写形容词硕士论文的人,我想你偶然发现了一个非常有趣的问题! 【参考方案1】:

我认为应该在 other answers 提到的内容中添加一点是,虽然

List&lt;Dog&gt; 不是 List&lt;Animal&gt; 在 Java 中

这也是事实

A list of dogs is-a list of animals 英文(合理解释下)

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

换一种说法:Java 中的List&lt;Dog&gt; 在英文中并不表示“狗的列表”,它的意思是“狗的列表,除了狗之外什么都没有”。

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

【讨论】:

是的,人类语言更加模糊。但是,一旦您将不同的动物添加到狗列表中,它仍然是动物列表,但不再是狗列表。不同之处在于,具有模糊逻辑的人类通常可以毫无问题地意识到这一点。 作为一个发现与数组的持续比较更加令人困惑的人,这个答案为我钉上了它。我的问题是语言直觉。 我认为混淆源于“woozle 列表”一词是否指可用于存储 woozles 的 container 的问题,该容器包含以下容器每个都有一个 woozle,或一个 woozle 容器的内容,一个 woozle 容器的内容,或包含在它们集合中的 woozle 容器的聚合内容。英文短语“woozles 列表”通常指的是最后一个,但编程语言中的相关结构通常指的是其他结构。【参考方案2】:

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

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

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

这里要注意的一点是,takeAnimals() 方法被定义为接受任何动物类型的东西,它可以接受一个 Animal 类型的数组,它也可以接受一个 Dog 数组,因为 Dog-is-a -动物。这就是多态的作用。

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

现在说我们稍微调整一下代码并使用 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());
        
       

上面的类将编译并产生输出 -

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-a-Animal 类型的任何 ArrayList,所以它不应该在这里破坏交易。

但是,不幸的是,编译器会抛出错误,并且不允许我们将 Dog ArrayList 传递给期望 Animal ArrayList 的变量。

你问为什么?

因为想象一下,如果 JAVA 允许将 Dog ArrayList - 狗 - 放入 Animal ArrayList - 动物 - 然后在 takeAnimals() 方法中,有人会执行类似的操作 -

animals.add(new Cat());

认为这应该是可行的,因为理想情况下它是一个 Animal ArrayList,并且您应该可以将任何猫添加到它作为 Cat-is-also-a-Animal,但实际上您将 Dog 类型的 ArrayList 传递给它。

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

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

【讨论】:

【参考方案3】:

该问题已被正确识别为与差异有关,但详细信息不正确。纯函数列表是协变数据函子,这意味着如果类型 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)

【讨论】:

【参考方案4】:

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

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

当您想在构造函数中传递列表或对其进行迭代时,这很有用。

【讨论】:

这会产生比它实际解决的问题更多的问题 如果您尝试将猫添加到列表中,肯定会产生问题,但出于循环目的,我认为这是唯一不冗长的答案。【参考方案5】:

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?

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

// 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&lt;Animal&gt; animals = new ArrayList&lt;&gt;(dogs); 之后,您不能随后将animals 直接分配给dogscats

// These are both illegal
dogs = animals;
cats = animals;

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

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

【讨论】:

【参考方案6】:

您要查找的内容称为 covariant type 参数。这意味着如果一种类型的对象可以在方法中替换为另一种类型(例如,Animal 可以替换为 Dog),同样适用于使用这些对象的表达式(因此 List&lt;Animal&gt; 可以替换为 @ 987654325@)。问题是协方差对于一般的可变列表是不安全的。假设您有一个List&lt;Dog&gt;,它被用作List&lt;Animal&gt;。当您尝试将 Cat 添加到 List&lt;Animal&gt;(实际上是 List&lt;Dog&gt;)时会发生什么?自动允许类型参数协变会破坏类型系统。

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

【讨论】:

【参考方案7】:

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

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

【讨论】:

【参考方案8】:

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

所以假设有一个方法如下:

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

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

因此,从本质上讲,这种行为是被实现的,因此人们不能将错误的东西添加到集合中。现在我相信类型擦除的存在是为了与没有泛型的遗留 java 兼容....

【讨论】:

【参考方案9】:

对于参数化类型,子类型是invariant。更难的是类DogAnimal 的子类型,参数化类型List&lt;Dog&gt; 不是List&lt;Animal&gt; 的子类型。相比之下,covariant 子类型被数组使用,所以数组 类型 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 所说,此代码是非法的,否则它会违反类型约束,在预期狗时返回猫。

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

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

代码是合法的。然而,抛出一个array store exception。 一个数组在运行时以这种方式携带它的类型 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_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

观察方法体的翻译代码是相同的。编译器将每个参数化类型替换为其erasure。此属性至关重要,它不会破坏向后兼容性。

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

【讨论】:

【参考方案10】:

问题已得到很好的识别。但是有一个解决方案;使 doSomething 通用:

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

现在您可以使用 List 或 List 或 List 调用 doSomething。

【讨论】:

【参考方案11】:

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

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

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

【讨论】:

【参考方案12】:

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

在大多数情况下,我们可以使用Collection&lt;? extends T&gt; 而不是Collection&lt;T&gt;,这应该是首选。但是,我发现这样做并不容易。关于这是否总是最好的做法还有待商榷。我在这里展示了一个类 DownCastCollection,它可以将 Collection&lt;? extends T&gt; 转换为 Collection&lt;T&gt;(我们可以为 List、Set、NavigableSet 定义类似的类),以便在使用标准方法非常不方便时使用。下面是一个如何使用它的示例(在这种情况下我们也可以使用Collection&lt;? extends Object&gt;,但我会保持简单说明使用 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;


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


【讨论】:

这是个好主意,以至于它已经存在于 Java SE 中。 ; ) Collections.unmodifiableCollection 对,但是我定义的集合可以修改。 可以,可以修改。 Collection&lt;? extends E&gt; 已经正确处理了该行为,除非您以非类型安全的方式使用它(例如,将其转换为其他东西)。我看到的唯一优势是,当您调用 add 操作时,即使您强制转换它也会引发异常。【参考方案13】:

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

List&lt;Dog&gt; 不是 List&lt;Animal&gt; 的子类。但是 Dog[] Animal[] 的子类。

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

    // 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

对于泛型,反之亦然:泛型是 erased 并且是不变的。 因此,泛型不能提供运行时类型安全,但它们提供编译时类型安全。 在下面的代码中,如果泛型是协变的,则可以在第 3 行创建 heap pollution。

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

【讨论】:

有人可能会说,正因为如此,Arrays in Java are broken, 协变数组是编译器的“特性”。【参考方案14】:

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

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

该代码可以正常编译,但会引发运行时错误(第二行中的java.lang.ArrayStoreException: java.lang.Boolean)。它不是类型安全的。泛型的重点是添加编译时类型安全性,否则您可以坚持使用没有泛型的普通类。

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

【讨论】:

可以说,数组协方差是一个语言设计错误。请注意,由于类型擦除,泛型收集在技术上不可能实现相同的行为。 "我想说泛型的全部意义在于它不允许这样做。"。您永远无法确定:Java and Scala's Type Systems are Unsound: The Existential Crisis of Null Pointers (presented at OOPSLA 2016)(似乎已更正)【参考方案15】:

不,List&lt;Dog&gt;不是List&lt;Animal&gt;。考虑一下你可以用List&lt;Animal&gt; 做什么——你可以向它添加任何动物......包括一只猫。现在,你能合乎逻辑地将一只猫添加到一窝小狗中吗?绝对不是。

// 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&lt;? extends Animal&gt;,因为您不知道它是List&lt;Cat&gt;。您可以检索一个值并知道它将是一个Animal,但您不能添加任意动物。 List&lt;? super Animal&gt; 则相反——在这种情况下,您可以安全地向其添加 Animal,但您不知道可以从中检索到什么,因为它可能是 List&lt;Object&gt;

【讨论】:

有趣的是,就像直觉告诉我们的那样,每个狗列表都是确实是动物列表。关键是,并非每个动物列表都是狗列表,因此通过添加猫来改变列表是问题所在。 @Ingo:不,不是真的:您可以将猫添加到动物列表中,但不能将猫添加到狗列表中。如果您以只读方式考虑,则狗列表只是动物列表。 @JonSkeet - 当然,但是谁规定从猫和狗的列表中创建一个新列表实际上会改变狗的列表?这是 Java 中的任意实现决策。与逻辑和直觉背道而驰。 @Ingo:我不会用“当然”开始。如果你有一个列表,上面写着“我们可能想去的酒店”,然后有人在上面添加了一个游泳池,你认为这有效吗?不 - 这是酒店列表,而不是建筑物列表。我什至没有说过“狗的列表不是动物的列表”——我把它用代码术语,用代码字体。我真的不认为这里有任何歧义。无论如何使用子类都是不正确的——这是关于分配兼容性,而不是子类化。 @ruakh: 问题是你在执行时会考虑一些可以在编译时被阻塞的东西。而且我认为数组协方差从一开始就是一个设计错误。【参考方案16】:

让我们以 JavaSE tutorial 为例

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 个选项可以解决这个问题:

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

    1234563你这样做有编译错误)。

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

【讨论】:

【参考方案17】:

其实你可以用一个接口来实现你想要的。

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

【讨论】:

【参考方案18】:

List&lt;Dog&gt; 不是List&lt;Animal&gt; 的原因是,例如,您可以将Cat 插入List&lt;Animal&gt;,但不能插入List&lt;Dog&gt;...您可以使用通配符尽可能使泛型更具可扩展性;例如,从List&lt;Dog&gt; 读取类似于从List&lt;Animal&gt; 读取——但不是写入。

Generics in the Java Language 和 Section on Generics from the Java Tutorials 对为什么有些东西是或不是多态的或允许使用泛型有一个非常好的、深入的解释。

【讨论】:

【参考方案19】:

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

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

听起来不错,不是吗?但是对于Animals,您只能通过Consumers 和Suppliers。如果您有Mammal 消费者,但有Duck 供应商,则它们不应该适合,尽管两者都是动物。为了禁止这种情况,我们添加了额外的限制。

我们必须定义我们使用的类型之间的关系。

E. g.,

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 消费者。

【讨论】:

4 个版本中,#2 可能不正确。例如我们不能用(Consumer&lt;Runnable&gt;, Supplier&lt;Dog&gt;) 调用它,而DogAnimal &amp; Runnable 的子类型

以上是关于List<Dog> 是 List<Animal> 的子类吗?为啥 Java 泛型不是隐式多态的?的主要内容,如果未能解决你的问题,请参考以下文章

JAVA list中如何将类型为子类的list引用赋值给类型为父类的list引用?

List集合

java 两个list可以相加吗

是列表 List的子类 ?为什么Java泛型不是隐式多态的?

面向对象集合篇

mybatis循环插入数据问题