【问题标题】:Any simple way to explain why I cannot do List<Animal> animals = new ArrayList<Dog>()? [duplicate]有什么简单的方法可以解释为什么我不能做 List<Animal> animals = new ArrayList<Dog>()? [复制]
【发布时间】:2011-01-21 18:01:39
【问题描述】:

我知道为什么不应该这样做。但是有没有办法向外行解释为什么这是不可能的。您可以轻松地向外行解释:Animal animal = new Dog();。狗是一种动物,但狗的列表不是动物的列表。

【问题讨论】:

标签: java generics oop covariance


【解决方案1】:

List 是一个对象,您可以在其中插入任何动物,例如猫或章鱼。 ArrayList 不是。

【讨论】:

    【解决方案2】:

    假设你可以这样做。有人提交了List&lt;Animal&gt; 合理地期望能够做的事情之一就是向其添加Giraffe。当有人尝试将Giraffe 添加到animals 时会发生什么?运行时错误?这似乎违背了编译时类型的目的。

    【讨论】:

    • 如果我在其中添加Giraffe,为什么会出现运行时错误?如果我这样做animals.get() 我只能期待animalGiraffeanimal。在ArrayList&lt;Dog&gt;() 中添加Giraffe 会很奇怪,但我没有看到任何运行时错误。所有类型都在运行时被擦除。
    • 运行时错误将来自其他可能仍在使用您的 ArrayList 的代码,并且正确地认为它只包含 Dogs。此外,从理论的角度来看,擦除是这种情况下的一个实现细节。
    • @PSpeed - 是的,如果您在 Rune 的回答中单独引用 ArrayList&lt;Dog&gt;,那将是正确的。如果没有,一切都会好起来的,对吧?
    • 那为什么要声明 ArrayList 而不是 ArrayList。否则,您正在颠覆打字系统……那有什么意义呢? :)
    • ...此外,就像我说的,Java 正在擦除 ArrayList 的类型这一事实是一个实现细节。如果某个其他类 Foo 实际上并没有删除它的类型,因为它可能在构造函数中使用它,那么它可以检查类型并给你运行时错误。 Java 不提供真正类型安全的集合这一事实主要是向后兼容性问题。
    【解决方案3】:

    请注意,如果您有

    List<Dog> dogs = new ArrayList<Dog>()
    

    如果可以的话

    List<Animal> animals = dogs;
    

    不会dogs 变成List&lt;Animal&gt;。动物底层的数据结构仍然是ArrayList&lt;Dog&gt;,所以如果你尝试将Elephant插入animals,你实际上是在将它插入ArrayList&lt;Dog&gt;,这是行不通的(大象显然也是如此)大;-)。

    【讨论】:

    • 是的,如果您有单独的 ArrayList&lt;Dog&gt; 引用,那将是正确的。如果没有,一切都会好起来的,对吧?
    【解决方案4】:

    通过继承,您实际上是在为多个类创建通用类型。这里有一个常见的动物类型。您通过创建一个 Animal 类型的数组并保留相似类型的值(继承类型 dog、cat 等)来使用它。

    例如:

     dim animalobj as new List(Animal)
      animalobj(0)=new dog()
       animalobj(1)=new Cat()
    

    .......

    知道了吗?

    【讨论】:

      【解决方案5】:

      假设您创建了一个列表。然后,您将其声明为 List 并将其交给同事。他,并非没有道理,相信他可以在里面放一只

      然后他把它还给你,你现在有一个列表,中间有一只。混乱随之而来。

      请务必注意,由于列表的可变性,存在此限制。在 Scala 中(例如),您可以声明 Dogs 列表是 Animals 列表。这是因为 Scala 列表(默认情况下)是不可变的,因此将 Cat 添加到 Dogs 列表中将为您提供 列表strong>动物。

      【讨论】:

      • 我喜欢这个因为“狗和猫住在一起”的参考。尽管我认为“确保”可能意味着“随之而来”。注意:一旦完全理解了这个答案,这就是进入 List extends Animal> 用于并且它对可以调用哪些方法的限制变得更加明显(即:不能调用 add() 但可以调用 get() 等)
      • 确实如此。现已编辑。
      • 此外,这个答案似乎是唯一一个将其描述为接近“向外行解释的方式”的答案。虽然我指出了潜在的拼写错误,但第二句中加粗的“列表”可能需要某种“动物”属性。
      • 谢谢。迷失在格式中!
      • 很好的答案,被接受为“随之而来的混乱”。
      【解决方案6】:

      您正在寻找的答案与称为协变和逆变的概念有关。一些语言支持这些(例如 .NET 4 增加了支持),但一些基本问题通过如下代码演示:

      List<Animal> animals = new List<Dog>();
      
      animals.Add(myDog); // works fine - this is a list of Dogs
      animals.Add(myCat); // would compile fine if this were allowed, but would crash!
      

      因为 Cat 是从 animal 派生的,所以编译时检查会建议它可以添加到 List。但是,在运行时,您不能将 Cat 添加到 Dogs 列表中!

      因此,虽然看起来很简单,但这些问题实际上是非常复杂的。

      这里有一个关于 .NET 4 中协/逆变的 MSDN 概述:http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx - 它也适用于 java,虽然我不知道 Java 的支持是什么样的。

      【讨论】:

        【解决方案7】:

        您正在尝试执行以下操作:

        List<? extends Animal> animals = new ArrayList<Dog>()
        

        应该可以的。

        【讨论】:

        • 这能解释给外行吗?我不这么认为。
        【解决方案8】:

        这是因为泛型类型是invariant

        【讨论】:

          【解决方案9】:

          首先,让我们定义我们的动物王国:

          interface Animal {
          }
          
          class Dog implements Animal{
              Integer dogTag() {
                  return 0;
              }
          }
          
          class Doberman extends Dog {        
          }
          

          考虑两个参数化接口:

          interface Container<T> {
              T get();
          }
          
          interface Comparator<T> {
              int compare(T a, T b);
          }
          

          以及这些TDog 的实现。

          class DogContainer implements Container<Dog> {
              private Dog dog;
          
              public Dog get() {
                  dog = new Dog();
                  return dog;
              }
          }
          
          class DogComparator implements Comparator<Dog> {
              public int compare(Dog a, Dog b) {
                  return a.dogTag().compareTo(b.dogTag());
              }
          }
          

          Container 接口的上下文中,您的要求是相当合理的:

          Container<Dog> kennel = new DogContainer();
          
          // Invalid Java because of invariance.
          // Container<Animal> zoo = new DogContainer();
          
          // But we can annotate the type argument in the type of zoo to make
          // to make it co-variant.
          Container<? extends Animal> zoo = new DogContainer();
          

          那么为什么 Java 不自动执行此操作呢?考虑一下这对Comparator 意味着什么。

          Comparator<Dog> dogComp = new DogComparator();
          
          // Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats!
          // Comparator<Animal> animalComp = new DogComparator();
          
          // Invalid Java, because Comparator is invariant in T
          // Comparator<Doberman> dobermanComp = new DogComparator();
          
          // So we introduce a contra-variance annotation on the type of dobermanComp.
          Comparator<? super Doberman> dobermanComp = new DogComparator();
          

          如果 Java 自动允许将 Container&lt;Dog&gt; 分配给 Container&lt;Animal&gt;,那么人们还会期望 Comparator&lt;Dog&gt; 可以分配给 Comparator&lt;Animal&gt;,这是没有意义的 - Comparator&lt;Dog&gt; 怎么能比较两个猫?

          那么ContainerComparator 有什么区别?容器产生 T 类型的值,而Comparator 消耗 它们。这些对应于类型参数的协变逆变用法。

          有时在两个位置都使用类型参数,使界面不变

          interface Adder<T> {
             T plus(T a, T b);
          }
          
          Adder<Integer> addInt = new Adder<Integer>() {
             public Integer plus(Integer a, Integer b) {
                  return a + b;
             }
          };
          Adder<? extends Object> aObj = addInt;
          // Obscure compile error, because it there Adder is not usable
          // unless T is invariant.
          //aObj.plus(new Object(), new Object());
          

          出于向后兼容性的原因,Java 默认为不变性。您必须在变量、字段、参数或方法返回的类型上使用? extends X? super X 显式选择适当的差异。

          这真的很麻烦——每次有人使用泛型类型时,他们都必须做出这个决定!当然ContainerComparator 的作者应该能够一劳永逸地声明这一点。

          这称为“声明站点差异”,可在 Scala 中使用。

          trait Container[+T] { ... }
          trait Comparator[-T] { ... }
          

          【讨论】:

            【解决方案10】:

            我能给出的最好的外行回答是:因为在设计泛型时,他们不想重复对 Java 的数组类型系统做出的相同决定,使其不安全

            这可以通过数组实现:

            Object[] objArray = new String[] { "Hello!" };
            objArray[0] = new Object();
            

            这段代码编译得很好,因为数组的类型系统在 Java 中的工作方式。它会在运行时引发ArrayStoreException

            决定不允许泛型出现这种不安全的行为。

            另见其他地方:Java Arrays Break Type Safety,许多人认为它是Java Design Flaws 之一。

            【讨论】:

              【解决方案11】:

              如果您无法更改列表,那么您的推理将非常合理。不幸的是,List&lt;&gt; 被强制操作。这意味着您可以通过添加新的Animal 来更改List&lt;Animal&gt;。如果您被允许使用List&lt;Dog&gt; 作为List&lt;Animal&gt;,您最终可能会得到一个还包含Cat 的列表。

              如果List&lt;&gt; 不能突变(就像在 Scala 中一样),那么您可以将 A List&lt;Dog&gt; 视为 List&lt;Animal&gt;。例如,C# 使用协变和逆变泛型类型参数使这种行为成为可能。

              这是更一般的Liskov substitution principal 的一个实例。

              突变导致您出现问题的事实发生在其他地方。考虑类型SquareRectangle

              SquareRectangle 吗?当然——从数学的角度来看。

              您可以定义一个Rectangle 类,它提供可读的getWidthgetHeight 属性。

              您甚至可以根据这些属性添加计算其areaperimeter 的方法。

              然后您可以定义一个 Square 类,它是 Rectangle 的子类,并使 getWidthgetHeight 返回相同的值。

              但是当您开始通过setWidthsetHeight 允许突变时会发生什么?

              现在,Square 不再是 Rectangle 的合理子类。改变其中一个属性将不得不默默地改变另一个以保持不变性,并且将违反 Liskov 的替换原则。更改 Square 的宽度会产生意想不到的副作用。为了保持正方形,您还必须更改高度,但您只要求更改宽度!

              当您可以使用Rectangle 时,您不能使用您的Square。因此,存在突变Square 不是Rectangle

              您可以在Rectangle 上创建一个新方法,该方法知道如何克隆具有新宽度或新高度的矩形,然后您的Square 可以在克隆过程中安全地转移到Rectangle,但是现在您不再改变原始值。

              List&lt;Dog&gt; 的界面允许您向列表中添加新项目时,List&lt;Dog&gt; 也不能是List&lt;Animal&gt;

              【讨论】:

                【解决方案12】:

                我想说最简单的答案是忽略猫和狗,它们无关紧要。重要的是列表本身。

                List<Dog> 
                

                List<Animal> 
                

                是不同的类型,Dog 派生自 Animal 与此无关。

                此声明无效

                List<Animal> dogs = new List<Dog>();
                

                出于同样的原因

                AnimalList dogs = new DogList();
                

                虽然 Dog 可能继承自 Animal,但由

                生成的列表类
                List<Animal> 
                

                不继承自

                生成的列表类
                List<Dog>
                

                假设因为两个类是相关的,所以将它们用作泛型参数会使这些泛型类也相关是错误的。虽然您当然可以将狗添加到

                List<Animal>
                

                这并不意味着

                List<Dog> 
                

                的子类
                List<Animal>
                

                【讨论】:

                  【解决方案13】:

                  英文答案:

                  如果'List&lt;Dog&gt;List&lt;Animal&gt;',则前者必须支持(继承)后者的所有操作。添加猫可以对后者进行,但不能对前者进行。所以“是”关系失败了。

                  编程答案:

                  类型安全

                  防止这种损坏的保守语言默认设计选择:

                  List<Dog> dogs = new List<>();
                  dogs.add(new Dog("mutley"));
                  List<Animal> animals = dogs;
                  animals.add(new Cat("felix"));  
                  // Yikes!! animals and dogs refer to same object.  dogs now contains a cat!!
                  

                  为了具有子类型关系,必须满足“可转换性”/“可替代性”标准。

                  1. 合法对象替换 - 后代支持祖先的所有操作:

                    // Legal - one object, two references (cast to different type)
                    Dog dog = new Dog();
                    Animal animal = dog;  
                    
                  2. 合法的集合替换 - 对祖先的所有操作都支持在后代上:

                    // Legal - one object, two references (cast to different type)
                    List<Animal> list = new List<Animal>()
                    Collection<Animal> coll = list;  
                    
                  3. 非法泛型替换(类型参数转换) - 后代中不支持的操作:

                    // Illegal - one object, two references (cast to different type), but not typesafe
                    List<Dog> dogs = new List<Dog>()
                    List<Animal> animals = list;  // would-be ancestor has broader ops than decendant
                    

                  然而

                  根据泛型类的设计,类型参数可以在“安全位置”使用,这意味着转换/替换有时可以成功而不会破坏类型安全。协变意味着泛型实例 G&lt;U&gt; 可以替换 G&lt;T&gt; 如果 U 是 T 的相同类型或子类型。逆变意味着泛型实例 G&lt;U&gt; 可以替换 G&lt;T&gt; 如果 U 是相同类型或 T 的超类型。这些是安全的两种情况的位置:

                  • 协变位置:

                    • 方法返回类型(泛型类型的输出) - 子类型必须具有同等/更多限制性,因此它们的返回类型符合祖先
                    • 不可变字段的类型(由所有者类设置,然后是“仅内部输出”)- 子类型必须更具限制性,因此当它们设置不可变字段时,它们符合祖先

                    在这些情况下,允许使用这样的后代替换类型参数是安全的:

                    SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
                    SomeCovariantType<? extends Animal> ancestor = decendant;
                    

                    通配符加上'extends'给出了使用站点指定的协方差。

                  • 逆向位置:

                    • 方法参数类型(泛型类型的输入)- 子类型必须同等/更适应,以便在传递祖先的参数时不会中断
                    • 类型参数上限(内部类型实例化)- 子类型必须同等/更适应,因此当祖先设置变量值时它们不会中断

                    在这些情况下,允许使用这样的祖先替换类型参数是安全的:

                    SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
                    SomeContravariantType<? super Dog> ancestor = decendant;
                    

                    通配符加上“super”给出使用站点指定的逆变。

                  使用这两个习语需要开发人员付出额外的努力和关注才能获得“替代能力”。 Java 需要手动开发人员来确保类型参数分别真正用于协变/逆变位置(因此是类型安全的)。我不知道为什么-例如scala编译器检查这个:-/。你基本上是在告诉编译器“相信我,我知道我在做什么,这是类型安全的”。

                  • 不变的位置

                    • 可变字段的类型(内部输入和输出) - 可以被所有祖先和子类型类读写 - 读是协变的,写是逆变的;结果是不变的
                    • (如果类型参数同时用于协变和逆变位置,则这会导致不变性)

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2011-08-11
                    • 2014-01-09
                    • 2012-04-08
                    • 2015-05-29
                    • 2014-07-22
                    • 2017-05-08
                    • 1970-01-01
                    • 2019-03-15
                    相关资源
                    最近更新 更多