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

fro*_*die 727 java generics polymorphism inheritance

我对Java泛型如何处理继承/多态性感到困惑.

假设以下层次结构 -

动物(父母)

- (儿童)

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

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

Jon*_*eet 870

不,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?
Run Code Online (Sandbox Code Playgroud)

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

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

  • @Ingo:不,不是真的:你可以将猫添加到动物列表中,但是你不能将猫添加到狗列表中.如果您认为狗是只读的,那么只有动物列表才会列出. (58认同)
  • 有趣的是,每个狗的名单*确实是一个动物名单,就像直觉告诉我们的那样.关键是,并非每个动物列表都是狗的列表,因此通过添加猫来改变列表就是问题所在. (46认同)
  • @ruakh:问题是你正在向执行时间提供一些可以在编译时阻止的东西.我认为阵列协方差是一个设计错误. (13认同)
  • @JonSkeet - 当然,但是谁强制要求从猫和狗列表中制作新名单实际上会改变狗的名单?这是Java中的任意实现决策.一个违背逻辑和直觉的人. (11认同)
  • @Ingo:我不会开始使用"肯定".如果你有一个列表,在顶部显示"我们可能想去的酒店",然后有人添加了一个游泳池,你认为这有效吗?不 - 这是酒店列表,不是建筑物清单.并且它不像我甚至说"狗列表不是动物列表" - 我用代码字体*代码*.我真的不认为这里有任何含糊之处.无论如何,使用子类是不正确的 - 它是关于赋值兼容性,而不是子类化. (7认同)
  • @Ingo:重点是我们在Java*的上下文中说*,这些内容都是在指定的.我不认为从这种背景中取出它真的很有帮助.(甚至在现实生活中,如果你开始在列表中添加"错误类型的项目",在各种情况下,列表不再有意义.) (4认同)
  • 我认为这一论点的问题是`List <Animal>`的契约实际上*没有*指定你可以添加任何`Animal`.允许列表实现完全不可变,或者是固定长度,或者对可以添加的元素(例如,基于其运行时类型)具有任意限制.请注意,`Dog []`*是*Animal []`,禁止分配在运行时被阻止.(通常,由于擦除,"List <Dog>"无法完成.)如果我们想将此视为语言设计失败以外的其他内容,我认为我们需要*[续]* (4认同)
  • *[续]*使用像'Comparable <Dog>`这样的例子,有理由说`Comparable <Dog>`("可以与任何`Dog`比较")并不意味着`Comparable <Animal>` ("可以与任何'动物'相比"). (2认同)
  • Java允许在使用数组时执行该示例。在编译时,您将不会有任何问题。但是在运行时将抛出强制转换异常。请解释一下为什么使用数组而不是通用列表可以做到这一点。我认为这种行为是不一致的。我可以想象Java开发人员在引入泛型时决定不再保留此行为。 (2认同)
  • @GinoBambino:首先,我认为数组的协方差是一个错误.(这是.NET复制的一个错误,令我烦恼.)其次,随着泛型在Java中的实现方式,它*不能在执行时被捕获 - 一个`String []`知道它真的是一个`String [ ]`而不是`Object []`,但是`ArrayList <String>`不知道.至于为什么在引入泛型时数组行为没有改变 - 这将是一个*大规模*突破性变化.这永远不会发生. (2认同)
  • @atomAltera:不,因为泛型的目的是隐式地添加预期有效的强制类型转换。 (2认同)
  • @Ingo:直觉失败的原因是它适用于不可变列表,而不是共享对可变列表的引用。如果您确保不变性,那么Java提供了这样的视图,即您可以编写`List &lt;Animal&gt; animals = Collections.unmodifiableList(dogs);`。由于“动物”视图不允许突变,因此很安全。 (2认同)
  • @Yar:我不希望列表将“猫”“变形”为“狗”。我想-和*做*有-说的方式,“嘿,我只消费列表,所有东西都应该是动物”-所以您使用`List &lt;?扩展了Animal&gt;`,并且您没有添加到列表中。对我来说似乎很好。通常,尝试使编程语言与“人类的直觉”相匹配听起来很不错,直到您意识到它变得模棱两可,并且每个人都有不同的直觉。 (2认同)

Mic*_*and 79

您正在寻找的是协变型参数.这意味着如果一种类型的对象可以替换方法中的另一种类型(例如,Animal可以替换为Dog),则同样适用于使用这些对象的表达式(因此List<Animal>可以替换为List<Dog>).问题是一般来说协方差对于可变列表是不安全的.假设你有一个List<Dog>,它被用作一个List<Animal>.当你试图将猫添加到这个会发生什么事List<Animal>是真的List<Dog>?自动允许类型参数协变会破坏类型系统.

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


Mic*_*yan 44

a List<Dog>不是a 的原因是List<Animal>,例如,你可以插入Cata List<Animal>,但不能插入List<Dog>...你可以使用通配符使generics在可能的情况下更具可扩展性; 例如,从a读取List<Dog>类似于读取List<Animal>- 但不是写作.

Java语言的泛型从Java教程泛型科有一个很好的,深入的解释,为什么有些东西是或不是多晶型或泛型允许的.


ein*_*ica 34

我认为应该在其他 答案中提到的一点是,尽管如此

List<Dog>不是Java中的一个List<Animal>

这也是事实

狗的清单是 - 英文动物清单(嗯,在合理的解释下)

OP的直觉起作用的方式 - 当然是完全有效的 - 是后一句.但是,如果我们应用这种直觉,我们会在其类型系统中获得一种非Java风格的语言:假设我们的语言允许将猫添加到我们的狗列表中.那是什么意思?这意味着该名单不再是狗的名单,而只是一个动物名单.以及一系列哺乳动物和一系列四足动物.

换句话说:List<Dog>Java中的A 并不是指英语中的"狗列表",它的意思是"可以有狗的列表,而不是其他".

更一般地说,OP的直觉适用于对象的操作可以改变其类型的语言,或者更确切地说,对象的类型是其值的(动态)函数.

  • 是的,人类的语言更加模糊。但是,一旦您将不同的动物添加到狗列表中,它仍然是动物列表,但不再是狗列表。不同之处在于,具有模糊逻辑的人类通常可以毫无问题地意识到这一点。 (5认同)
  • 作为一个发现与数组的不断比较更加令人困惑的人,这个答案对我来说很明确。我的问题是语言直觉。 (2认同)

Yis*_*hai 32

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

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;
Run Code Online (Sandbox Code Playgroud)

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

现在有你需要更灵活倍,这是什么? super Class? extends Class是.前者是需要插入类型Collection(例如)时,后者是需要以类型安全的方式从中读取时.但同时做两者的唯一方法就是拥有一种特定的类型.

  • 可以说,数组协方差是一种语言设计错误.请注意,由于类型擦除,通用集合在技术上无法实现相同的行为. (13认同)

out*_*dev 12

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

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

数组是可恢复的和协变的.
可恢复意味着它们的类型信息在运行时完全可用.
因此,数组提供运行时类型安全性但不提供编译时类型安全性.

    // 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
Run Code Online (Sandbox Code Playgroud)

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

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

  • 可能有人认为,正因为如此,[Java中的数组被打破](/sf/ask/865897021/#12370259), (2认同)

Mik*_*kis 10

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

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

所以,从Java 10版本开始这个问题的解决方案如下:

(注:S = 超类)

List<S> supers = List.copyOf( descendants );
Run Code Online (Sandbox Code Playgroud)

如果完全安全,则此函数将执行强制转换;如果强制转换不安全,则执行复制。结果列表是不可修改的。

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


glg*_*lgl 7

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

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}
Run Code Online (Sandbox Code Playgroud)

听起来不错,不是吗?但是你只能为s传递Consumers 和s。如果您有消费者,但有供应商,则尽管两者都是动物,但它们不应该适合。为了禁止这种情况,增加了额外的限制。SupplierAnimalMammalDuck

代替上面的,我们必须定义我们使用的类型之间的关系。

例如,

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

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

OTOH,我们也可以

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

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

我们甚至可以做到

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

其中,具有直观的关系Life- > Animal- > Mammal- > DogCat等等,我们甚至可以在一MammalLife消费者,而不是StringLife消费者。


Hit*_*esh 5

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

因此,假设有一种如下所示的方法:

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

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

因此,从本质上讲,这种行为是可以实现的,因此不能将错误的东西添加到集合中。现在,我相信存在类型擦除,以便与不带泛型的传统Java兼容。


Roo*_*t G 5

子类型对于参数化类型来说是不变的。即使该类Dog是 的子类型Animal,参数化类型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 error
Dog dog = dogs.get(0);
Run Code Online (Sandbox Code Playgroud)

正如 @Jon Skeet 所说,这段代码是非法的,因为否则它会在狗期望的时候返回猫,从而违反类型约束。

将上面的代码与数组的类似代码进行比较是有启发性的。

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

代码是合法的。但是,会引发数组存储异常。数组在运行时携带其类型,这样 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

使用命令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
}
Run Code Online (Sandbox Code Playgroud)

观察方法体的翻译代码是相同的。编译器通过擦除来替换每个参数化类型。此属性至关重要,意味着它不会破坏向后兼容性。

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


归档时间:

查看次数:

103795 次

最近记录:

6 年 前