为什么有些人声称Java的泛型实现很糟糕?

sde*_*sse 111 java generics

我偶尔听说过,通用泛型,Java并没有把它弄好.(最近的参考,这里)

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

Jon*_*eet 141

坏:

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

好:

  • 通配符允许在呼叫侧指定协方差/逆变,这在许多情况下非常简洁
  • 它总比没有好!

  • 同样好:引入时没有破坏现有代码. (53认同)
  • @Paul:我想我实际上更喜欢暂时的破损,但之后是一个不错的API.或者采用.NET路由并引入新的集合类型.(2.0中的.NET泛型没有破坏1.1应用程序.) (51认同)
  • 但其缺点是巨大的,永久性的.有一个很大的短期胜利,以及IMO的长期损失. (36认同)
  • 乔恩 - "它总比没有好"是主观的:) (11认同)
  • 有了一个很大的现有代码库,我喜欢这样一个事实:我可以开始更改一个方法的签名以使用泛型,而不会改变所有调用它的人. (6认同)
  • @Rogerio:你声称我的第一个"坏"项目不正确.我的第一个"坏"项目表示信息在编译时丢失.因为这是不正确的,你必须说信息*不会在编译时丢失......至于混淆其他开发人员,你似乎是唯一一个被我所说的混淆的人. (6认同)
  • 来吧:你的陈述毫无条件地说"类型信息在编译时会丢失".对于新类型声明,参数声明,返回类型声明和字段声明(只要它们实际指定类型),这根本不是真的.例如,当我将方法参数声明为List <String>时,该信息显然不会"在编译时丢失". (4认同)
  • @Rogerio:不管你多次向我提出类型擦除的问题,你仍然不能问一个*对象*它的类型参数是什么.该信息*在编译时丢失.你可以问一个*字段*它的类型参数是什么,但这不是一回事. (3认同)
  • 我是第二个上述评论!并且想补充一点,运行时检查是性能损失,它(运行时检查)也可以通过Collections api获得. (2认同)
  • @Dimitris:但是如果A是B的子类型,那么`List <A>`*不是*List <B>`的子类型.你可以调用`list.Add(new A())`清单<A>`.你最好不要在`List <B>`上这样做.如果你只是从当前列表中取*,这很好,但这取决于调用者正在做什么......这就是为什么指定所需方差的调用者在时间上是整齐的.然而,这对其他人来说是一种痛苦. (2认同)
  • @Jon Skeet我会把它添加到你的列表中:Java泛型不像C#泛型那样类型安全(参见Paul Tomblin的回答),打字可以在运行时被规避(也许使用丑陋的黑客,但它可以).此外,一些函数的输入奇怪地是不完整的并且容易隐藏bug:谁设计了`List <T> .remove`来接受任何Object作为参数?(我看到一个真正的bug以这种方式隐藏.)(我猜罪魁祸首是向后兼容性......)结果,我从来没有100%确定我的Java通用代码避免抛出错误...而我感到安全C#泛型. (2认同)
  • 是的。Java 引入泛型已经 14 年了。我想将 json 反序列化为泛型对象,但这完全是一团糟(您必须声明泛型对象,并且必须将泛型类型参数传递给构造函数,因为您无法在运行时找出构造函数中的泛型类型。 ) 太好了... java 泛型提供了一个短期的胜利,不必破坏现有代码,但现在对于永恒的其余部分,所有未来的代码都必须承受这种垃圾架构的负担。 (2认同)

Pau*_*lin 26

最大的问题是Java泛型只是一个编译时的东西,你可以在运行时颠覆它.C#受到称赞,因为它进行了更多的运行时检查.这篇文章中有一些非常好的讨论,它与其他讨论有关.

  • 他是对的。你的类比是错误的,因为设计这艘船的人应该设计它来爬山。他们的设计目标并没有很好地考虑到所需的内容。现在我们都只有一条船,无法爬山。 (4认同)
  • @VincentCantin——这绝对是一个问题。因此,我们都在抱怨它。Java 泛型是半生不熟的。 (4认同)

Jar*_*Par 17

主要问题是Java在运行时实际上没有泛型.这是一个编译时功能.

当您在Java中创建泛型类时,他们使用一个名为"Type Erasure"的方法来实际从类中删除所有泛型类型,并基本上用Object替换它们.泛型版本的泛型是编译器只要在方法体中出现时,只需将强制转换插入指定的泛型类型.

这有很多缺点.恕我直言,最大的一个就是你不能使用反射来检查泛型类型.类型在字节代码中实际上不是通用的,因此不能作为泛型进行检查.

这里的差异很好概述:http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

  • 呃,因为你*可以*使用反射来查看泛型类型,泛型方法签名.您无法使用反射来检查与参数化实例关联的通用信息.例如,如果我有一个方法foo(List <String>)的类,我可以反思地找到"String" (3认同)
  • 我不确定你为什么被否决。据我了解,您是对的,并且该链接非常有用。投票赞成。 (2认同)

cle*_*tus 14

  1. 运行时实现(即不是类型擦除);
  2. 使用原始类型的能力(这与(1)有关);
  3. 虽然通配符很有用,但语法和知道何时使用它会让很多人感到困惑.和
  4. 没有性能提升(因为(1); Java泛型是铸造对象的语法糖).

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

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}
Run Code Online (Sandbox Code Playgroud)

然后声明两个变量:

MyClass<T> m1 = ...
MyClass m2 = ...
Run Code Online (Sandbox Code Playgroud)

现在打电话getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 
Run Code Online (Sandbox Code Playgroud)

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

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

public class Enum<T extends Enum<T>>
Run Code Online (Sandbox Code Playgroud)

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

  • 哇,这让我的思绪震惊了几秒钟.+1 (3认同)

Cli*_*ler 9

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

Map someMap;
Run Code Online (Sandbox Code Playgroud)

现在,我必须声明它

Map<String, List<String>> someMap;
Run Code Online (Sandbox Code Playgroud)

每次我把它传递给某种方法时,我都要重复那个重要的长篇宣言.在我看来,所有额外的打字都会分散开发人员的注意力并将他带出"区域".此外,当代码充满了大量的瑕疵时,有时很难再回到它并迅速筛选所有的瑕疵以找到重要的逻辑.

Java作为常用的最冗长的语言之一已经声名狼借,泛型只是增加了这个问题.

那些额外的冗长你真的买了什么?有多少次你有一个问题,有人把一个Integer放入一个应该保存字符串的集合中,或者有人试图从一个整数集合中提取一个字符串?在我10年的构建商业Java应用程序的经验中,这从来就不是错误的重要来源.所以,我不太清楚你为了额外的冗长而得到什么.它真的只是让我感到额外的官僚包袱.

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

List someList = someMap.get("some key");
Run Code Online (Sandbox Code Playgroud)

我要做

List someList = (List) someMap.get("some key");
Run Code Online (Sandbox Code Playgroud)

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

我认为如果1)它没有添加泛型但是2)允许从超类型到子类型的隐式转换,Java会好一些.在运行时捕获不正确的强制转换.然后我可以有简单的定义

Map someMap;
Run Code Online (Sandbox Code Playgroud)

后来呢

List someList = someMap.get("some key");
Run Code Online (Sandbox Code Playgroud)

所有的残余都将消失,我真的不认为我会在我的代码中引入一个新的大错误来源.

  • 对不起,我不同意你所说的一切.但我不打算将其投票,因为你认为它很好. (13认同)
  • "你真正为那些额外的冗长而买什么?......"它告诉可怜的维护程序员这些收藏中应该是什么.我发现这是使用商业Java应用程序的问题. (4认同)
  • "有多少次你有一个问题,有人把一个[x]放进了一个应该持有[y] s的集合中?" - 哦,我已经失去了数!即使没有错误,它也是一个可读性杀手.扫描许多文件以找出对象将会是什么(或调试)确实会让我脱离该区域.你可能会喜欢Haskell - 即使是强大的打字但是不那么残忍(因为类型是推断的). (3认同)
  • 当然,有很多用Python和Ruby等语言构建的大型成功系统的示例,它们完全可以完成我在答案中所建议的。 (2认同)

jdk*_*off 8

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


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }
Run Code Online (Sandbox Code Playgroud)

--jeffk ++


Ant*_*lev 6

在编译时检查Java泛型的正确性,然后删除所有类型信息(该过程称为类型擦除.因此,泛型List<Integer>将简化为其原始类型,非泛型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");
Run Code Online (Sandbox Code Playgroud)


Zac*_*ena 5

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

还有一些人还认为 Java 的泛型实现将语言的复杂性增加到了不可接受的水平(参见 Ken Arnold 的“泛型被认为是有害的”)。Angelika Langer 的泛型常见问题解答很好地说明了事情会变得多么复杂。


Szy*_*zga 5

Java 泛型仅在编译时存在,并被编译为非泛型代码。在 C# 中,实际编译的 MSIL 是通用的。这对性能有巨大影响,因为 Java 仍然在运行时进行转换。请参阅此处了解更多信息


Lap*_*son 5

我希望这是一个维基,这样我就可以添加给其他人……但是……

问题:

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


Nat*_*Nat 5

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

这编译:

List<Integer> x = Collections.emptyList();
Run Code Online (Sandbox Code Playgroud)

但这是一个语法错误:

foo(Collections.emptyList());
Run Code Online (Sandbox Code Playgroud)

其中foo定义为:

void foo(List<Integer> x) { /* method body not important */ }
Run Code Online (Sandbox Code Playgroud)

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

  • javac的不一致推断是废话. (3认同)
  • 我认为后一种形式被拒绝的原因是因为方法重载可能存在多个版本的"foo". (3认同)
  • 这种批评不再适用,因为Java 8引入了改进的目标类型推断 (2认同)