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

Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?

提问人:froadie 提问时间:4/30/2010 最后编辑:cellepofroadie 更新时间:11/3/2022 访问量:138056

问:

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

假设以下层次结构 -

动物(父母)

- (儿童)

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

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

Java 泛型 继承 多态性

评论

22赞 froadie 4/30/2010
还有一个完全不相关的语法问题现在困扰着我——我的标题应该是“为什么不是 Java 的泛型”还是“为什么不是 Java 的泛型”??“泛型”是因为 s 是复数,还是因为它是一个实体而单数?
28赞 SyntaxT3rr0r 4/30/2010
在 Java 中完成的泛型是一种非常糟糕的参数多态形式。不要像我以前那样对他们太有信心,因为总有一天你会重创他们可怜的局限性:外科医生伸出手<手术刀>,手<海绵>KABOOM!计算 [TM]。这是 Java 泛型的限制。任何 OOA/OOD 都可以很好地翻译成 Java(使用 Java 接口可以很好地完成 MI),但泛型不能很好地完成它。它们适用于“集合”和过程编程(这是大多数 Java 程序员所做的,所以......
8赞 rai.skumar 12/4/2012
List<Dog> 的超类不是 List<Animal>而是 List<?>(即未知类型的列表)。泛型擦除已编译代码中的类型信息。这样做是为了让使用泛型(java 5 及更高版本)的代码与没有泛型的早期版本的 java 兼容。
5赞 Aniket Thakur 10/3/2015
相关的 SO 问题 - 说 < 有什么用? 扩展 SomeObject>而不是 <SomeObject>
10赞 dantiston 5/30/2017
@froadie因为似乎没有人回应......它绝对应该是“为什么不是 Java 的泛型......”。另一个问题是,“generic”实际上是一个形容词,因此“generics”指的是由“generic”修饰的去复数名词。你可以说“该函数是泛型的”,但这比说“该函数是泛型的”更麻烦。但是,说“Java 具有泛型函数和类”,而不仅仅是“Java 具有泛型”有点麻烦。作为一个写了关于形容词的硕士论文的人,我想你偶然发现了一个非常有趣的问题!

答:

1055赞 Jon Skeet 4/30/2010 #1

不,a 不是 .考虑一下你可以用一个做什么 - 你可以添加任何动物......包括一只猫。现在,你能合乎逻辑地在一窝小狗中添加一只猫吗?绝对不行。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?

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

现在,您不能将 a 添加到 a,因为您不知道它是 .您可以检索一个值并知道它将是 ,但不能添加任意动物。反之亦然 - 在这种情况下,您可以安全地向它添加一个,但您对可能从中检索到的内容一无所知,因为它可能是 .CatList<? extends Animal>List<Cat>AnimalList<? super Animal>AnimalList<Object>

评论

61赞 Ingo 1/29/2013
有趣的是,每一份狗的名单确实都是一份动物名单,就像直觉告诉我们的那样。关键是,并非每个动物列表都是狗列表,因此通过添加猫来改变列表是问题所在。
81赞 Jon Skeet 1/29/2013
@Ingo:不,不是真的:你可以将猫添加到动物列表中,但你不能将猫添加到狗列表中。如果您从只读的角度考虑,狗列表只是动物列表。
16赞 Ingo 1/29/2013
@JonSkeet - 当然,但是谁强制要求从猫和狗名单中列出新名单实际上会改变狗名单?这是 Java 中的任意实现决策。一个与逻辑和直觉背道而驰的人。
8赞 Jon Skeet 1/29/2013
@Ingo:我不会一开始就用“当然”这个词。如果你有一个列表,上面写着“我们可能想去的酒店”,然后有人在上面添加了一个游泳池,你认为这有效吗?不,这是酒店列表,而不是建筑物列表。这不像我什至说“狗的名单不是动物的名单”——我用代码术语,用代码字体来表达。我真的不认为这里有任何歧义。无论如何,使用子类都是不正确的 - 这是关于赋值兼容性,而不是子类。
20赞 Jon Skeet 7/4/2013
@ruakh:问题在于,你把一些可能在编译时被阻止的东西推到了执行时间。我认为数组协方差一开始就是一个设计错误。
111赞 Michael Ekstrand 4/30/2010 #2

您要查找的内容称为协变类型参数。这意味着,如果在方法中可以将一种类型的对象替换为另一种对象(例如,可以替换为 ),则这同样适用于使用这些对象的表达式(因此可以替换为 )。问题在于,协方差对于可变列表通常是不安全的。假设您有一个 ,并且它被用作 .当您尝试将猫添加到其中时会发生什么,这确实是一只?自动允许类型参数协变会破坏类型系统。AnimalDogList<Animal>List<Dog>List<Dog>List<Animal>List<Animal>List<Dog>

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

51赞 Michael Aaron Safyan 4/30/2010 #3

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

Java 语言中的泛型和 Java 教程中的泛型部分对为什么某些东西是多态的或不是多态的或泛型允许的有很好的深入解释。

38赞 Yishai 4/30/2010 #4

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

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

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

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

评论

13赞 Michael Borgwardt 4/30/2010
可以说,数组协方差是一个语言设计错误。请注意,由于类型擦除,从技术上讲,对于泛型集合来说,不可能有相同的行为。
1赞 David Tonhofer 8/16/2017
"我想说的是,泛型的全部意义在于它不允许这样做。你永远无法确定:Java 和 Scala 的类型系统是不健全的:空指针的存在危机(在 OOPSLA 2016 上提出)(似乎已经纠正了)
0赞 Matthew Read 5/14/2022
事实上。Reified 泛型可以从根本上防止这种情况,但 Java 的非类型擦除泛型则不能。 两者都只是 的拙劣伪装,其内置的安全性为零;如果你能绕过编译检查(非常简单)或创建一个无法应用编译检查的设置(也很容易),你就可以把事情搞砸了。List<Dog>List<Animal>List
7赞 Hitesh 12/4/2012 #5

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

因此,假设有如下方法:

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

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

因此,从本质上讲,这种行为的实现是为了防止将错误的东西添加到集合中。现在我相信存在类型擦除,以便与没有泛型的旧 java 兼容......

50赞 einpoklum 3/30/2013 #6

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

List<Dog>在 Java 中不是 a-aList<Animal>

这也是事实

狗的清单是-英文的动物清单(根据合理的解释)

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

换句话说:Java 中的 A 在英语中不是“狗的列表”,而是“狗的列表,除了狗之外别无他物”。List<Dog>

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

评论

5赞 Vlasec 11/13/2017
是的,人类的语言更加模糊。但是,一旦你将不同的动物添加到狗的列表中,它仍然是一个动物列表,但不再是狗的列表。不同的是,一个具有模糊逻辑的人通常可以毫无问题地意识到这一点。
2赞 FLonLon 2/25/2019
作为一个发现与数组的不断比较更加令人困惑的人,这个答案为我钉住了它。我的问题是语言直觉。
1赞 supercat 10/22/2020
我认为这种混淆源于一个问题,即术语“woozle列表”是指可用于存储woozle的容器,还是容纳每个woozle容器的容器的容器,或者woozle容器的内容,woozle容器的容器的内容,或者它们的集合中保存的woozle容器的聚合内容。英语短语“list of woozles”最常指的是其中的最后一个,但编程语言中的相关结构通常指的是其他结构之一。
3赞 dan b 12/15/2014 #7

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

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

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

}

}

评论

0赞 Radiodef 5/9/2015
这是个好主意,以至于它已经存在于 Java SE 中。;) 集合.unmodifiableCollection
1赞 dan b 5/9/2015
没错,但是我定义的集合可以修改。
0赞 Vlasec 11/13/2017
是的,它可以被修改。 但是,已经正确处理了该行为,除非您以非类型安全的方式使用它(例如,将其转换为其他内容)。我看到的唯一优点是,当您调用该操作时,即使您强制转换它,它也会引发异常。Collection<? extends E>add
8赞 glglgl 2/15/2015 #8

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

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

听起来不错,不是吗?但是你只能传递 s 和 s 换 s。如果你有一个消费者,但有一个供应商,他们不应该适合,尽管两者都是动物。为了禁止这样做,添加了其他限制。ConsumerSupplierAnimalMammalDuck

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

例如,

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

我们反过来说:我们定义了 的类型,并限制它可以放入 .SupplierConsumer

我们甚至可以做到

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

其中,具有直观关系 -> -> -> 等,我们甚至可以将 A 放入消费者中,但不能将 A 放入消费者中。LifeAnimalMammalDogCatMammalLifeStringLife

评论

1赞 ZhongYu 7/28/2015
在 4 个版本中,#2 可能是不正确的。例如,我们不能用 while 来调用它 是 的子类型(Consumer<Runnable>, Supplier<Dog>)DogAnimal & Runnable
4赞 Renato Probst 1/29/2016 #9

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

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

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

评论

2赞 Ferrybig 2/1/2016
这将产生比实际解决更多的问题
2赞 Renato Probst 2/2/2016
如果您尝试将猫添加到列表中,肯定会产生问题,但出于循环目的,我认为这是唯一不冗长的答案。
0赞 aurelius 2/27/2016 #10

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

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

  2. 将子类型视为它的超类型,并在编译“添加”方法时进行限制(因此,在 drawAll 方法中,如果要传递圆形列表,形状的子类型,编译器应该检测到它,并用编译错误限制您这样做)。

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

23赞 outdev 9/30/2017 #11

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

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

数组是可重构和协变的。
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

对于泛型,反之亦然:
泛型被擦除且不变
因此,泛型不能提供运行时类型安全性,但它们提供编译时类型安全性。
在下面的代码中,如果泛型是协变的,则有可能在第 3 行造成堆污染

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

评论

4赞 leonbloy 12/15/2017
也许有人会说,正因为如此,Java 中的数组被破坏了
1赞 Cristik 3/2/2018
协变数组是编译器的“特征”。
0赞 Cristik 3/2/2018 #12

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

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

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

0赞 gerardw 3/29/2018 #13

这个问题已经得到了很好的确定。但是有一个解决方案;make doSomething 通用:

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

现在,您可以使用 List<Dog> 或 List<Cat> 或 List<Animal> 调用 doSomething。

评论

0赞 ggb667 11/3/2022
是的,除了做某事可能取决于狗的特定行为。
5赞 Görkem Mülayim 5/3/2018 #14

对于参数化类型,子类型是不变的。即使硬类是 的子类型,参数化类型也不是 的子类型。相反,数组使用协变子类型,因此数组 type 是 的子类型。DogAnimalList<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 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];

该代码是合法的。但是,会引发数组存储异常。 数组在运行时以这种方式携带其类型,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");
    }
}

使用命令 ,这将显示以下 Java 字节码:javap -c Demonstration

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
}

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

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

0赞 ejaenv 7/20/2018 #15

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

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
0赞 Luke Hutchison 11/20/2019 #16

继 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<Animal> animals = new ArrayList<>(dogs);animalsdogscats

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

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

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

2赞 Yttrill 11/1/2020 #17

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

0赞 Shubham Arya 12/4/2020 #18

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

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

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

该类编译良好,当我们运行上述类时,我们得到输出

animal eating
animal eating
animal eating
animal eating
animal eating
animal eating

这里需要注意的一点是,takeAnimals() 方法被定义为接受任何 Animal 类型的内容,它可以接受 Animal 类型的数组,也可以接受 Dog 的数组,因为 Dog-is-a-Animal。这就是多态性在起作用。

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

现在假设我们稍微调整一下我们的代码并使用 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 - dogs - 放入 Animal ArrayList - animals - 然后在 takeAnimals() 方法中有人做类似 -

animals.add(new Cat());

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

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

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

8赞 Mike Nakis 5/11/2022 #19

其他人已经很好地解释了为什么你不能只将后代列表转换为超类列表。

然而,许多人访问这个问题是为了寻找解决方案。

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

(注:S = 超类)

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

如果这样做是完全安全的,则此函数将执行强制转换,如果强制转换不安全,则执行复制。

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

评论

1赞 ggb667 11/3/2022
我喜欢这个,因为它比 superclassThingList = new Array(subclassOfThingList);