【问题标题】:Java Generics: Cannot cast List<SubClass> to List<SuperClass>? [duplicate]Java 泛型:无法将 List<SubClass> 转换为 List<SuperClass>? [复制]
【发布时间】:2011-03-15 20:08:27
【问题描述】:

刚刚遇到这个问题:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // compile error: incompatible type

DataNode 类型是 Tree 的子类型。

public class DataNode implements Tree

令我惊讶的是,这适用于数组:

DataNode[] a2 = new DataNode[0];
Tree[] b2 = a2;   // this is okay

这有点奇怪。任何人都可以对此作出解释吗?

【问题讨论】:

    标签: java generics


    【解决方案1】:

    您在第二种情况下看到的是数组协方差。这是 IMO 的一件坏事,它使数组中的分配不安全 - 尽管在编译时很好,但它们在执行时可能会失败。

    在第一种情况下,假设代码确实编译了,然后是:

    b1.add(new SomeOtherTree());
    DataNode node = a1.get(0);
    

    你希望发生什么?

    你可以这样做:

    List<DataNode> a1 = new ArrayList<DataNode>();
    List<? extends Tree> b1 = a1;
    

    ...因为那样你就只能从b1 获取东西,并且它们保证与Tree 兼容。你不能准确地调用b1.add(...),因为编译器不知道它是否安全。

    查看this section of Angelika Langer's Java Generics FAQ 了解更多信息。

    【讨论】:

    • 啊..!这有效:列表
    • 感谢您的解释。您能否举一个“在数组中不安全 - 尽管在编译时很好,但它们可能在执行时失败”的示例? - 忽略它。我得到了这个例子。
    • @SiLent SoNG:是的,基本上和我的“破碎”示例完全相同,但在数组中设置一个值:)
    • 我遇到了类似的情况,经过大量搜索后准备发布一个新的q,但遇到了这个。我尝试了一个独立的 Java 类,它具有: 1. List x = new ArrayList(); 2. List x = new ArrayList(); #1 编译正常,但 #2 给出错误:找到不兼容的类型:java.util.ArrayList required: java.util.List List x = new ArrayList();这令人费解,但感谢Java Generics FAQ 它解决了这个谜! :)
    • @CDT:啊,我明白了。我建议你然后阅读 Java 泛型教程 - 你会看到很多:)
    【解决方案2】:

    如果您必须从List&lt;DataNode&gt; 转换为List&lt;Tree&gt;,并且您知道这样做是安全的,那么实现此目的的一种丑陋方法是进行双重转换:

    List&lt;DataNode&gt; a1 = new ArrayList&lt;DataNode&gt;();

    List&lt;Tree&gt; b1 = (List&lt;Tree&gt;) (List&lt;? extends Tree&gt;) a1;

    【讨论】:

      【解决方案3】:

      简短的解释:最初允许它用于数组是一个错误。

      更长的解释:

      假设这是允许的:

      List<DataNode> a1 = new ArrayList<DataNode>();
      List<Tree> b1 = a1;  // pretend this is allowed
      

      那么我不能继续:

      b1.add(new TreeThatIsntADataNode()); // Hey, b1 is a List<Tree>, so this is fine
      
      for (DataNode dn : a1) {
        // Uh-oh!  There's stuff in a1 that isn't a DataNode!!
      }
      

      现在一个理想的解决方案将允许您在使用只读的List 的变体时进行您想要的类型转换,但在使用可读写的接口(如List)时不允许它。 Java 不允许在泛型参数上使用这种差异表示法 (*),但即使这样做,您也无法将 List&lt;A&gt; 转换为 List&lt;B&gt;,除非 AB 相同。

      (*) 即写类时不允许。您可以将变量声明为 List&lt;? extends Tree&gt; 类型,这很好。

      【讨论】:

        【解决方案4】:

        List&lt;DataNode&gt; 不会扩展 List&lt;Tree&gt;,即使 DataNode 扩展了 Tree。那是因为在您的代码之后您可以执行 b1.add(SomeTreeThatsNotADataNode),这将是一个问题,因为那时 a1 中也会有一个不是 DataNode 的元素。

        你需要使用通配符来实现这样的事情

        List<DataNode> a1 = new ArrayList<DataNode>();
        List<? extends Tree> b1 = a1;
        b1.add(new Tree()); // compiler error, instead of runtime error
        

        另一方面,DataNode[] 确实扩展了 Tree[]。当时这似乎是合乎逻辑的事情,但你可以这样做:

        DataNode[] a2 = new DataNode[1];
        Tree[] b2 = a2; // this is okay
        b2[0] = new Tree(); // this will cause ArrayStoreException since b2 is actually a DataNode[] and can't store a Tree
        

        这就是为什么当他们将泛型添加到 Collections 时,他们选择稍微不同以防止运行时错误。

        【讨论】:

          【解决方案5】:

          在设计数组时(即几乎在设计 java 时),开发人员认为方差会很有用,所以他们允许了它。然而这个决定经常被批评,因为它允许你这样做(假设NotADataNodeTree的另一个子类):

          DataNode[] a2 = new DataNode[1];
          Tree[] b2 = a2;   // this is okay
          b2[0] = new NotADataNode(); //compiles fine, causes runtime error
          

          因此,当设计泛型时,就决定泛型数据结构应该只允许显式变化。 IE。你不能做List&lt;Tree&gt; b1 = a1;,但你可以做List&lt;? extends Tree&gt; b1 = a1;

          但是,如果您执行后者,尝试使用 addset 方法(或任何其他将 T 作为参数的方法)将导致编译错误。这样就不可能编译上述数组问题的等价物(没有不安全的强制转换)。

          【讨论】:

          • 这解释了数组和泛型之间的设计差异。这正是我看到 Jon Skeet 的解释后想问的!感谢您的回答。
          【解决方案6】:

          简短回答: 列表 a1 与列表 b2 的类型不同; 在 a1 中,您可以放置​​任何扩展 DataNode 的对象类型。所以它可能包含树以外的其他类型。

          【讨论】:

            【解决方案7】:

            这是来自C#的答案,但我认为这实际上并不重要,因为原因是一样的。

            “特别是,与数组类型不同,构造的引用类型不表现出“协变”转换。这意味着即使 B 是派生的,类型 List 也没有转换(隐式或显式)到 List从 A。同样,不存在从 List 到 List 的转换。

            这样做的理由很简单:如果允许转换为 List,那么显然可以将 A 类型的值存储到列表中。但这会破坏 List 类型列表中的每个对象始终是 B 类型的值这一不变性,否则在分配到集合类时可能会发生意外失败。”

            http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e

            【讨论】:

            • 原因是有点相同 - 但Java允许不同形式的差异。
            【解决方案8】:

            DataNode 可能是 Tree 的子类型,但 List DataNode 不是 List Tree 的子类型。

            https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html

            【讨论】:

              【解决方案9】:

              这是使用类型擦除实现泛型的经典问题。

              假设您的第一个示例确实有效。然后,您将能够执行以下操作:

              List<DataNode> a1 = new ArrayList<DataNode>();
              List<Tree> b1 = a1;  // suppose this works
              b1.add(new Tree());
              

              但由于b1a1 指的是同一个对象,这意味着a1 现在指的是包含DataNodes 和Trees 的List。如果你尝试获取最后一个元素,你会得到一个异常(不记得是哪个)。

              【讨论】:

              • 这与类型擦除无关。 .NET 泛型没有类型擦除,但它们具有相同的基本问题。我想如果没有类型擦除,您可能在添加时会出现执行时间异常 - 但由于泛型的重点是将类型安全放在 compile 时,它并没有真正使这里有很大的不同。
              • 九年后,Scala 有了一个更加完善的类型系统,没有临时、隐式或简单的类型差异。向开发人员隐藏差异对初学者来说可能是件好事,但很快您就会意识到它们每天都会出现,我们需要明确说明这一点。
              【解决方案10】:

              好吧,老实说:惰性泛型实现。

              没有语义上的理由不允许你的第一次做作。

              顺便说一句,虽然我喜欢 C++ 中的模板,但泛型以及我们这里存在的那种愚蠢的限制是我放弃 Java 的主要原因。

              【讨论】:

              • 您的解释不正确,这就是您被否决的原因(不是我,我没有打扰)。现在我们来推测一下,您是因为过早放弃了 Java 而不了解 Java 泛型,还是因为不了解泛型而放弃了 Java。
              • 如果您没有给出答案并且您只是陈述您对这种情况的看法,则可以预期会投反对票。这对任何方式都没有帮助。
              • @Julian:不,我已经考虑过了,我仍然认为限制是有道理的考虑到List&lt;T&gt; 代表一个可以添加元素的可变类型。如果您愿意,您可以称其为糟糕的设计决策,但一旦做出该决定,通过防止向其中添加错误类型的元素来保持类型安全是非常有意义的。
              • @JonSkeet, @BobTurbo:整个问题是关于对象的可变性,因此我在自己的 cmets 中谈到它:它是否会对泛型的实现方式产生如此大的影响?大多数情况下,您需要将这种类型的演员表提供给会通读列表的第三方。可悲的事实是,问题的答案是不可变对象,而不是可变和不可读的 extends Fruit>,因为我们都知道使用这种泛型过滤器会产生多么丑陋的代码。乔恩,不要忽视你的回答,但对我来说,它忽略了真正的问题:语言缺乏不变性。
              • ...也许集合应该是一等公民,就像数组一样,但这完全是另一场争论,会导致运算符重载和其他像我这样“不理解”的“高级”主题泛型”肯定无法掌握。我坚持我的说法,即 Java 泛型在设计和实现方面如此有限和一年级学生没有语义上的理由。他们为了感知安全而牺牲了表现力,同时将他们的局限性隐藏在神秘的、类似数学的表达式后面 >.
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-07-02
              • 1970-01-01
              相关资源
              最近更新 更多