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

我偶尔听说,在使用泛型时,Java 并没有做对

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

24149 次浏览

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

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

最大的问题是 Java 泛型只是一个编译时的东西,您可以在运行时颠覆它。C # 之所以受到赞扬,是因为它执行了更多的运行时检查。在 这篇文章中有一些非常好的讨论,并且它链接到其他的讨论。

在 Java 中引入泛型是一项艰巨的任务,因为架构师试图在功能、易用性和向下兼容与遗留代码之间取得平衡。不出所料,各方不得不做出妥协。

还有一些人认为 Java 的泛型实现将语言的复杂性提高到了一个不可接受的水平(参见 Ken Arnold 的“ 泛型被认为是有害的”)。安吉丽卡兰格的 常见问题给出了一个相当好的想法,如何复杂的事情可以成为。

  1. 运行时实现(不是类型擦除) ;
  2. 使用基本类型的能力(这与(1)有关) ;
  3. 虽然通配符很有用,但是语法和知道何时使用它是很多人都难以理解的事情。还有
  4. 没有性能改进(因为(1) ; Java 泛型是 castingi 对象的语法糖)。

(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 泛型更好。

主要问题是 Java 在运行时并没有泛型,它只是一个编译时特性。

当你在 Java 中创建一个泛型类时,他们使用一个叫做“类型擦除”的方法从类中移除所有的泛型类型,并且实际上用 Object 替换它们。泛型的最高版本是,编译器只要在方法体中出现指定的泛型类型,就简单地插入对它的转换。

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

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

在编译时检查 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");

坏:

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

好:

  • 通配符允许在调用端指定协方差/逆方差,这在许多情况下都非常简洁
  • 总比什么都没有好!

Java 泛型仅在编译时被编译成非泛型代码。在 C # 中,实际编译的 MSIL 是通用的。这对性能有巨大的影响,因为 Java 仍然在运行时进行强制转换。更多信息请点击这里.

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

问题:

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

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


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

—— jeffk + +

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

Map someMap;

现在,我必须声明它是

Map<String, List<String>> someMap;

每次我把它传递给某个方法,我都要重复那个很长的声明。在我看来,所有这些额外的输入会分散开发人员的注意力,使他们脱离“专区”。此外,当代码中充满了大量的“拐点”时,有时很难在以后回过头来快速筛选所有的“拐点”来找到重要的逻辑。

Java 已经因为是最常用的冗长语言之一而臭名昭著,而泛型只是增加了这个问题。

你这么啰嗦,到底想买什么?有多少次你真的遇到过这样的问题: 有人把一个整数放到一个应该包含字符串的集合中,或者有人试图从一个整数集合中拉出一个字符串?在我从事构建商业 Java 应用程序的10年经验中,这从来不是一个大的错误来源。我不太确定你这么啰嗦能得到什么好处。我只是觉得这是个额外的官僚包袱。

现在我要引起争议了。我认为 Java 1.4中的集合最大的问题是需要到处进行类型转换。我认为这些类型转换是额外的、冗长的,与泛型有许多相同的问题。所以,举个例子,我不能

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

我必须这么做

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

当然,原因是 get ()返回一个 Object,它是 List 的超类型。因此,作业不能没有类型转换。再一次,想想这条规则能给你带来多少好处。根据我的经验,没什么。

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

Map someMap;

然后再做

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

所有的漏洞都会消失,而且我真的不认为我会在我的代码中引入一个大的新的漏洞来源。

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

结果显示:

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

但这是一个语法错误:

foo(Collections.emptyList());

Foo 的定义是:

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

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

如果你听 Java Posse # 279-采访 Joe Darcy 和 Alex Buckley,他们谈论这个问题。这也链接到 Neal Gafter 的一篇题为 Java 的具体泛型的博客文章:

很多人不满意 由路径引起的限制 泛型是用 Java 实现的。 具体来说,他们不满意 泛型类型参数不是 具体化: 它们不在 泛型已实现 使用擦除,其中泛型类型 参数在 运行时间。

那篇博客文章引用了一个较早的条目 通过删除的困惑: 答案部分,它强调了需求中关于 迁移兼容性的观点。

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