【问题标题】:Why do some claim that Java's implementation of generics is bad?为什么有些人声称 Java 的泛型实现很糟糕?
【发布时间】:2010-10-05 22:51:36
【问题描述】:

我偶尔听说过泛型,Java 做得不对。 (最近的参考,here

请原谅我的经验不足,但是什么能让他们变得更好?

【问题讨论】:

    标签: java generics


    【解决方案1】:

    不好:

    • 类型信息在编译时丢失,因此在执行时您无法判断它“意味着”是什么类型
    • 不能用于值类型(这是一个大问题 - 例如,在 .NET 中,List<byte> 确实由 byte[] 支持,并且不需要装箱)
    • 调用泛型方法的语法很糟糕 (IMO)
    • 约束的语法可能会令人困惑
    • 通配符通常令人困惑
    • 由于上述原因的各种限制 - 铸造等

    好:

    • 通配符允许在调用方指定协变/逆变,这在许多情况下都非常简洁
    • 聊胜于无!

    【讨论】:

    • @Paul:我想我实际上更喜欢暂时性的损坏,但之后会有一个不错的 API。或者采用 .NET 路线并引入新的集合类型。 (2.0 中的 .NET 泛型并没有破坏 1.1 应用程序。)
    • 拥有庞大的现有代码库,我喜欢这样一个事实,即我可以开始更改一种方法的签名以使用泛型,而无需更改调用它的每个人。
    • 但它的缺点是巨大的,而且是永久性的。 IMO 短期内大赢,长期亏损。
    • 乔恩 - “总比没有好”是主观的 :)
    • @Rogerio:你声称我的第一个“坏”项目不正确。我的第一个“坏”项目说信息在编译时丢失。如果这是不正确的,您必须说信息不会在编译时丢失...至于让其他开发人员感到困惑,您似乎是唯一一个对我所说的内容感到困惑的人。
    【解决方案2】:

    最大的问题是 Java 泛型只是编译时的东西,你可以在运行时颠覆它。 C# 之所以受到称赞,是因为它进行了更多的运行时检查。 this post 中有一些非常好的讨论,它链接到其他讨论。

    【讨论】:

    • 这不是一个真正的问题,它从来没有被设计用于运行时。就好像你说船是个问题,因为它们不能爬山。作为一门语言,Java 做了很多人需要的事情:以有意义的方式表达类型。对于运行时类型的强制,人们仍然可以传递Class 对象。
    • 他是对的。你的比喻是错误的,因为设计这艘船的人应该设计它来爬山。他们的设计目标并没有很好地考虑到需要什么。现在我们都被困在一条船上,无法爬山。
    • @VincentCantin -- 这绝对是个问题。因此,我们都在抱怨它。 Java 泛型是半生不熟的。
    【解决方案3】:

    主要问题是 Java 在运行时实际上没有泛型。这是一个编译时特性。

    当您在 Java 中创建泛型类时,他们使用一种称为“类型擦除”的方法来实际从类中删除所有泛型类型,并实质上将它们替换为 Object。泛型的最高版本是,只要它出现在方法体中,编译器就会简单地将转换插入到指定的泛型类型。

    这有很多缺点。恕我直言,最大的问题之一是您不能使用反射来检查泛型类型。类型在字节码中实际上不是泛型的,因此不能作为泛型进行检查。

    此处的差异概述:http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

    【讨论】:

    • 我不确定你为什么被否决。据我了解,您是对的,并且该链接非常有用。投票赞成。
    • @Jason,谢谢。我的原始版本中有一个错字,我想我因此而被否决了。
    • 错误,因为您可以使用反射来查看泛型类型,泛型方法签名。您只是不能使用反射来检查与参数化实例关联的通用信息。例如,如果我有一个带有方法 foo(List) 的类,我可以反射性地找到“String”
    • 有很多文章说类型擦除使查找实际类型参数变得不可能。可以说:对象 x = new X[Y]();你能说明给定 x 是如何找到类型 Y 的吗?
    • @oxbow_lakes - 不得不使用反射来查找泛型类型几乎与无法做到这一点一样糟糕。例如。这是一个糟糕的解决方案。
    【解决方案4】:
    1. 运行时实现(即不是类型擦除);
    2. 使用原始类型的能力(这与(1)有关);
    3. 虽然通配符很有用,但语法和知道何时使用它却难倒很多人。和
    4. 没有性能改进(因为 (1);Java 泛型是 casti 对象的语法糖)。

    (1) 导致一些非常奇怪的行为。我能想到的最好的例子是。假设:

    public class MyClass<T> {
      T getStuff() { ... }
      List<String> getOtherStuff() { ... }
    }
    

    然后声明两个变量:

    MyClass<T> m1 = ...
    MyClass m2 = ...
    

    现在拨打getOtherStuff():

    List<String> list1 = m1.getOtherStuff(); 
    List<String> list2 = m2.getOtherStuff(); 
    

    第二个的泛型类型参数被编译器剥离,因为它是原始类型(意味着未提供参数化类型),即使它与参数化类型没有任何关系

    我还会提到我最喜欢的 JDK 声明:

    public class Enum<T extends Enum<T>>
    

    除了通配符(这是一个混合包),我只是认为 .Net 泛型更好。

    【讨论】:

    • 但是编译器会告诉你,尤其是 rawtypes lint 选项。
    • 对不起,为什么最后一行是编译错误?我在 Eclipse 中搞砸了它并且不能让它在那里失败 - 如果我添加足够的东西来编译它的其余部分。
    • public class Redundancy&lt;R extends Redundancy&lt;R&gt;&gt; ;)
    • @oconnor0 这不是编译失败,而是编译器警告,原因不明:The expression of type List needs unchecked conversion to conform to List&lt;String&gt;
    • Enum&lt;T extends Enum&lt;T&gt;&gt; 起初可能看起来很奇怪/多余,但实际上非常有趣,至少在 Java/它的泛型的约束下。枚举有一个静态的values() 方法,它给出了一个类型为枚举的元素数组,而不是Enum,并且该类型由泛型参数确定,这意味着您需要Enum&lt;T&gt;。当然,这种类型只在枚举类型的上下文中才有意义,并且所有枚举都是Enum 的子类,因此您需要Enum&lt;T extends Enum&gt;。但是,Java 不喜欢将原始类型与泛型混合,因此为了保持一致性,Enum&lt;T extends Enum&lt;T&gt;&gt;
    【解决方案5】:

    我要抛出一个非常有争议的观点。泛型使语言复杂化,也使代码复杂化。例如,假设我有一个将字符串映射到字符串列表的映射。在过去,我可以简单地将其声明为

    Map someMap;
    

    现在,我必须将其声明为

    Map<String, List<String>> someMap;
    

    每次我将它传递给某个方法时,我都必须再次重复那个又长又长的声明。在我看来,所有额外的输入都会分散开发人员的注意力,并将他带出“区域”。此外,当代码中充满了大量杂乱无章的内容时,有时很难稍后再返回并快速筛选所有杂乱内容以找到重要的逻辑。

    Java 作为最常用的最冗长的语言之一已经声名狼藉,而泛型只会增加这个问题。

    对于所有这些额外的冗长,你真正买了什么?有多少次你真的遇到过有人将一个整数放入一个应该包含字符串的集合中,或者有人试图从一个整数集合中拉出一个字符串?在我 10 年构建商业 Java 应用程序的经验中,这从来都不是错误的主要来源。所以,我不确定你会因为额外的冗长而得到什么。这真的只是让我觉得额外的官僚包袱。

    现在我将变得非常有争议。我认为 Java 1.4 中集合的最大问题是必须在任何地方进行类型转换。我认为这些类型转换是额外的、冗长的东西,它们与泛型有许多相同的问题。所以,例如,我不能只是这样做

    List someList = someMap.get("some key");
    

    我必须这样做

    List someList = (List) someMap.get("some key");
    

    当然,原因是 get() 返回一个 Object,它是 List 的超类型。因此,如果没有类型转换,就无法进行分配。再一次,想想这条规则真正给你带来了多少。根据我的经验,不多。

    我认为如果 1) 它没有添加泛型,但 2) 允许从超类型到子类型的隐式转换,Java 会更好。让不正确的强制转换在运行时被捕获。那么我就可以简单地定义

    Map someMap;
    

    后来做

    List someList = someMap.get("some key");
    

    所有的麻烦都会消失,我真的不认为我会在我的代码中引入大量新的错误来源。

    【讨论】:

    • 对不起,我不同意你所说的一切。但我不会因为你论证得好而投反对票。
    • 当然,有很多成功的大​​型系统示例,这些系统使用 Python 和 Ruby 等语言构建,完全符合我在回答中的建议。
    • 我认为我对这种想法的全部反对并不是因为它本身就是一个坏主意,而是因为 Python 和 Ruby 等现代语言使开发人员的生活变得越来越容易,反过来,开发人员在智力上自满,最终对自己的代码的理解力较低。
    • “你真的买了那么多冗长的东西?...”它告诉可怜的维护程序员这些集合应该是什么。我发现这是使用商业 Java 应用程序的问题。
    • “你有多少次真正遇到问题,有人将 [x] 放入应该包含 [y]s 的集合中?” - 哦,男孩,我数不清了!此外,即使没有错误,它也是可读性杀手。并且扫描许多文件以找出对象将是什么(或调试)确实使我脱离了该区域。您可能喜欢 Haskell - 甚至是强大的类型,但不那么繁琐(因为类型是推断出来的)。
    【解决方案6】:

    它们是编译时而非运行时的另一个副作用是您不能调用泛型类型的构造函数。所以你不能用它们来实现一个泛型工厂......

    
       public class MyClass {
         public T getStuff() {
           return new T();
         }
        }
    

    --jeffk++

    【讨论】:

      【解决方案7】:

      忽略整个类型擦除混乱,指定的泛型不起作用。

      这样编译:

      List<Integer> x = Collections.emptyList();
      

      但这是一个语法错误:

      foo(Collections.emptyList());
      

      其中 foo 定义为:

      void foo(List<Integer> x) { /* method body not important */ }
      

      所以表达式类型是否检查取决于它是分配给局部变量还是方法调用的实际参数。这有多疯狂?

      【讨论】:

      • javac 的不一致推断是废话。
      • 我认为后一种形式被拒绝的原因是因为方法重载可能存在多个版本的“foo”。
      • 这个批评不再适用,因为 Java 8 引入了改进的目标类型推断
      【解决方案8】:

      在编译时检查 Java 泛型的正确性,然后删除所有类型信息(该过程称为类型擦除。因此,泛型 List&lt;Integer&gt; 将减少到它的 原始类型,非泛型List,可以包含任意类的对象。

      这导致能够在运行时将任意对象插入到列表中,并且现在无法分辨哪些类型被用作泛型参数。后者反过来导致

      ArrayList<Integer> li = new ArrayList<Integer>();
      ArrayList<Float> lf = new ArrayList<Float>();
      if(li.getClass() == lf.getClass()) // evaluates to true
        System.out.println("Equal");
      

      【讨论】:

        【解决方案9】:

        将泛型引入 Java 是一项艰巨的任务,因为架构师试图平衡功能、易用性以及与遗留代码的向后兼容性。不出所料,必须做出妥协。

        有些人还认为 Java 的泛型实现将语言的复杂性提高到了无法接受的程度(参见 Ken Arnold 的“Generics Considered Harmful”)。 Angelika Langer 的 Generics FAQs 给出了一个很好的想法,说明事情会变得多么复杂。

        【讨论】:

          【解决方案10】:

          我希望这是一个 wiki,所以我可以添加到其他人...但是...

          问题:

          • 类型擦除(无运行时可用性)
          • 不支持原始类型
          • 与注解不兼容(它们都是在 1.5 中添加的,我仍然不确定为什么注解除了加快功能之外还不允许泛型)
          • 与阵列不兼容。 (有时我真的很想做类似 Class&lt;?extend MyObject&gt;[] 之类的事情,但我不被允许)
          • 奇怪的通配符语法和行为
          • Java 类之间的泛型支持不一致。他们将它添加到大多数集合方法中,但每隔一段时间,您就会遇到一个它不存在的实例。

          【讨论】:

            【解决方案11】:

            Java 在运行时不强制使用泛型,仅在编译时强制使用。

            这意味着您可以做一些有趣的事情,例如将错误的类型添加到泛型集合中。

            【讨论】:

            • 如果你做到了,编译器会告诉你你搞砸了。
            • @Tom - 不一定。有一些方法可以欺骗编译器。
            • 你不听编译器告诉你什么吗? (见我的第一条评论)
            • @Tom,如果您有一个 ArrayList 并将其传递给采用简单列表的旧式方法(例如,遗留或第三方代码),则该方法可以添加任何内容它喜欢那个列表。如果它在运行时强制执行,你会得到一个异常。
            • 静态 void addToList(List list) { list.add(1); } List list = new ArrayList();添加列表(列表);编译,甚至在运行时工作。唯一遇到问题的时候是从列表中删除期望字符串并获得 int 时。
            【解决方案12】:

            Java 泛型仅在编译时使用,并被编译成非泛型代码。在 C# 中,实际编译的 MSIL 是通用的。这对性能有很大的影响,因为 Java 仍然在运行时强制转换。 See here for more.

            【讨论】:

              【解决方案13】:

              如果你听Java Posse #279 - Interview with Joe Darcy and Alex Buckley,他们会谈论这个问题。这还链接到 Neal Gafter 的一篇名为 Reified Generics for Java 的博客文章,上面写着:

              很多人不满意 方式造成的限制 泛型是用 Java 实现的。 具体来说,他们不满意 泛型类型参数不是 物化:它们不可用 运行。泛型已实现 使用擦除,其中泛型类型 参数被简单地删除 运行时。

              那篇博文引用了较早的条目Puzzling Through Erasure: answer section,它强调了要求中关于迁移兼容性的观点。

              目标是向后提供 源和兼容性 目标代码,以及迁移 兼容性。

              【讨论】:

                【解决方案14】:

                泛型的问题在于,IMO 使 Java API 中的方法签名的阅读和理解难度增加了 10 倍,但在健壮性方面并没有太多收获。事实上,他们可能应该采取其他方式,通过方法调用完全摆脱编译时类型兼容性检查,并将其留给开发人员来解决或处理运行时异常。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2018-12-17
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2011-03-16
                  • 1970-01-01
                  相关资源
                  最近更新 更多