为什么有人声称Java的泛型实现不好?


115

我偶尔听到,使用泛型,Java并没有正确地做到这一点。(最近参考,在这里

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


23
我认为没有理由关闭它;围绕泛型的所有废话,澄清一下Java和C#之间的区别可能会帮助人们躲开那些不了解情况的人。
罗布

Answers:


146

坏:

  • 类型信息在编译时会丢失,因此在执行时您无法分辨出它是什么意思
  • 不能用于值类型(这是一个很大的问题-在.NET中,List<byte>确实有一个byte[]作为,并且不需要装箱)
  • 调用通用方法的语法很烂(IMO)
  • 约束的语法可能会造成混淆
  • 通配符通常令人困惑
  • 由于上述各种限制-铸造等

好:

  • 通配符允许在调用方指定协方差/相反,在许多情况下这非常整洁
  • 总比没有好!

51
@Paul:我想我本来希望临时破损,但之后要有像样的API。或采用.NET路线并引入新的集合类型。(2.0中的.NET泛型没有破坏1.1应用程序。)
Jon Skeet

6
有了现有的庞大代码库,我喜欢这样一个事实,即我可以开始将一种方法的签名更改为使用泛型,而无需更改调用它的每个人。
Paul Tomblin,2009年

38
但是这样做的缺点是巨大的,而且是永久性的。国际海事组织(IMO)有一个巨大的短期胜利,一个长期损失。
乔恩·斯基特

11
乔恩-“总比没有好”是主观的:)
MetroidFan2002 2009年

6
@Rogerio:您声称我的第一个“不良”项目不正确。我的第一个“错误”项目说,信息在编译时会丢失。为了使这种说法不正确,您必须说信息在编译时不会丢失……至于使其他开发人员感到困惑,您似乎是唯一被我所说的困惑的人。
乔恩·斯基特

26

最大的问题是Java泛型仅是编译时的事情,您可以在运行时对其进行颠覆。C#之所以受到赞誉是因为它可以执行更多的运行时检查。这篇文章中有一些非常好的讨论,它可以链接到其他讨论。


这并不是真正的问题,它从来没有被用于运行时。好像您说船是一个问题,因为船不能爬山。作为一种语言,Java满足了许多人的需求:以有意义的方式表达类型。对于运行时类型增强,人们仍然可以传递Class对象。
文森特·坎丁

1
他是对的。您的论断是错误的,因为设计船的人应该将其设计为爬山。他们的设计目标并未针对需要的内容进行深思熟虑。现在我们都只能坐船,我们不能爬山。
WiegleyJ

1
@VincentCantin-绝对是一个问题。因此,我们都在抱怨它。Java泛型是半熟的。
Josh M.

17

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

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

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

有关这些差异的详细概述:http : //www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
我不确定你为什么被否决。据我了解,您是正确的,并且该链接非常有用。投票赞成。
杰森·杰克逊

@杰森,谢谢。我的原始版本中有一个错字,我想我对此很不满意。
JaredPar

3
错误,因为您可以使用反射来查看泛型类型,即泛型方法签名。您只是不能使用反射来检查与参数化实例关联的一般信息。例如,如果我有一个带有方法foo(List <String>)的类,则可以反射地找到“字符串”
oxbow_lakes

有很多文章说类型擦除使得不可能找到实际的类型参数。可以说:Object x = new X [Y](); 您能展示给定x时如何找到类型Y吗?
user48956

@oxbow_lakes-必须使用反射来查找泛型类型与无法做到这一点几乎一样糟糕。例如,这是一个糟糕的解决方案。
Josh M.

14
  1. 运行时实现(即不是类型擦除);
  2. 使用原始类型的能力(与(1)有关);
  3. 尽管通配符很有用,但语法和知道何时使用它却使很多人感到困惑。和
  4. 没有性能提高(由于(1); Java泛型是CastiObject的语法糖)。

(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中弄乱了它,并且不能让它在那里失败-如果我添加了足够的东西来进行其余编译。
oconnor0 '02

public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop 2010年

@ oconnor0不是编译失败,而是编译器警告,无明显原因:The expression of type List needs unchecked conversion to conform to List<String>
artbristol 2012年

Enum<T extends Enum<T>>乍看起来可能很奇怪/很冗长,但实际上非常有趣,至少在Java /它的泛型约束内。枚举有一个静态values()方法,提供一个将其元素数组类型化为枚举的数组,而不是Enum,该类型由通用参数确定,即您想要的类型Enum<T>。当然,键入仅在枚举类型的上下文中才有意义,并且所有枚举都是的子类Enum,因此需要Enum<T extends Enum>。但是,Java不喜欢将原始类型与泛型混合使用,从而Enum<T extends Enum<T>>保持一致性。
2014年

10

我将提出一个有争议的意见。泛型使语言复杂化,并使代码复杂化。例如,假设我有一个将字符串映射到字符串列表的映射。在过去,我可以简单地声明为

Map someMap;

现在,我必须声明为

Map<String, List<String>> someMap;

每次我将其传递给某种方法时,我都必须再次重复该大的长声明。我认为,所有这些额外的输入都会分散开发人员的注意力,并将其带离“区域”。另外,当代码充满了很多细节时,有时很难稍后再回到代码中,并迅速筛选所有细节以找到重要的逻辑。

Java已经成为最常用的最冗长的语言之一,因此已经有不好的声誉,而泛型只会增加这个问题。

那么,对于这些额外的冗长性,您真正买了什么?在某人将一个Integer放入应该保存Strings的集合中,或者有人试图从Integers集合中拉出String时,您真的遇到了多少次问题?在我从事构建商业Java应用程序的10年经验中,这从来没有成为错误的主要来源。因此,我不太确定您要得到的额外详细信息。这确实让我感到额外的官僚包bag。

现在,我将要引起争议。我认为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");

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


15
抱歉,我不同意您所说的一切。但我不会拒绝它,因为您认为很好。
Paul Tomblin,2009年

2
当然,有很多用Python和Ruby等语言构建的大型成功系统的示例,它们的工作完全符合我在回答中所建议的。
克林特·米勒

我认为我对这种想法的全部反对并不是说这本身不是一个坏主意,而是随着Python和Ruby之类的现代语言使开发人员的生活越来越轻松,开发人员反过来会变得自满,最终降低理解自己的代码。
丹涛

4
“那么,对于这些额外的冗长性,您真的会买到什么呢?……”它告诉那些可怜的维护程序员,这些系列中应该有什么。我发现在使用商业Java应用程序时这是一个问题。
richj 2010年

4
“您真的有多少次遇到问题,有人将[x]放入本应保存[y]的集合中?” -天哪,我输了!即使没有错误,它也是可读性杀手。扫描许多文件以找出对象将是什么(或调试)确实确实使我脱离了该区域。您可能会喜欢Haskell-即使打字强度高,但也不会显得笨拙(因为可以推断出类型)。
马丁·卡波迪奇

8

它们是编译时而不是运行时的另一个副作用是您不能调用泛型类型的构造函数。因此,您不能使用它们来实现通用工厂...


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

--jeffk ++


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");

5

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

问题:

  • 类型清除(无运行时可用性)
  • 不支持基本类型
  • 与注解不兼容(它们都在1.5中添加,我仍然不确定为什么注解除了促成功能外,不允许泛型)
  • 与数组不兼容。(有时我真的很想做类似Class的事情<?扩展了MyObject >[],但我不允许这样做)
  • Wierd通配符语法和行为
  • 通用支持在Java类之间不一致。他们将其添加到大多数collections方法中,但是有时您会遇到其中不存在该实例的实例。

5

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

这样编译:

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

但这是一个语法错误:

foo(Collections.emptyList());

foo定义为:

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

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


3
javac的不一致推断是胡扯。
mP。

3
我认为后一种形式被拒绝的原因是由于方法重载可能存在多个版本的“ foo”。
杰森·克赖顿

2
这种批评不再适用,因为Java 8引入了改进的目标类型推论
oldrinb

4

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

有些人还认为Java的泛型实现将语言的复杂性提高到了无法接受的水平(请参阅Ken Arnold的“ 认为有害泛型 ”)。Angelika Langer的泛型常见问题解答对于如何使事情变得复杂有一个很好的主意。


3

Java不会在运行时强制执行泛型,而仅在编译时强制执行。

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


1
如果您进行管理,编译器会告诉您您搞砸了。
汤姆·霍顿-钉路

@Tom-不一定。有许多方法可以欺骗编译器。
Paul Tomblin,2009年

2
您不听编译器告诉您的内容吗?(请参阅我的第一条评论)
Tom Hawtin-大头钉

1
@Tom,如果您有ArrayList <String>并将其传递给采用简单List的旧方法(例如,旧代码或第三方代码),则该方法可以将喜欢的任何内容添加到该List中。如果在运行时强制执行,则会出现异常。
Paul Tomblin,2009年

1
静态无效addToList(列表列表){list.add(1); } List <String> list = new ArrayList <String>(); addToList(list); 可以编译,甚至可以在运行时运行。您唯一看到问题的时间是从列表中删除期望一个字符串并获取一个整数的时间。
拉普利·安德森


2

如果您听Java Posse#279-采访Joe Darcy和Alex Buckley,他们会谈论这个问题。这也链接到Neal Gafter的博客文章Reified Generics for Java,内容

许多人对Java中实现泛型的方式所带来的限制感到不满意。具体来说,他们对通用类型参数未得到优化不满意:它们在运行时不可用。泛型使用擦除来实现,其中泛型类型参数在运行时被简单地删除。

该博客文章引用了一个较旧的条目,即“ Erasure Through Erasure:Answer”部分,其中强调了需求中的迁移兼容性

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.