Java泛型真的很笨拙吗?为什么?

时间:2012-01-05 06:16:00

标签: java generics

和我一起忍受一段时间。我知道这听起来有点主观和争论一段时间,但我发誓最后有一个问号,这个问题实际上可以客观地回答......

来自.NET和C#背景,近年来我被语法糖所破坏,泛型与扩展方法相结合,在许多.NET解决方案中提供常见问题。使C#泛型非常强大的一个关键特性是,如果其他地方有足够的信息,编译器可以推断类型参数,所以我几乎不必写出来。在您意识到保存了多少次击键之前,您不必编写多行代码。例如,我可以写

var someStrings = new List<string>();
// fill the list with a couple of strings...
var asArray = someStrings.ToArray();

和C#将知道我的意思是第一个varList<string>,第二个是string[].ToArray()真的是.ToArray<string>()

然后我来到Java。

我已经对Java泛型有了足够的了解,知道它们是根本不同的,除了编译器没有实际编译成通用代码这一事实 - 它剥离了类型参数并使其成为可能无论如何,以某种(非常复杂的)方式工作(我还没有真正理解)。但即使我知道 Java中的泛型是根本不同的,我也无法理解为什么这样的结构是必要的:

 ArrayList<String> someStrings = new ArrayList<String>();
 // fill the list with a couple of strings...
 String[] asArray = someStrings.toArray(new String[0]); // <-- HERE!

为什么在地球上必须实例化一个新的String[],其中没有任何元素,不会被用于任何东西,因为Java编译器知道它是{{ 1}}而不是我想要的任何其他类型的数组?

我意识到这是重载的方式,String[]当前返回toArray()。但是,当Java的这一部分被发明时,为什么要做出这个决定呢?为什么这个设计比跳过完全返回Object[]重载的.toArray()并且只返回Object[] toArray()更好?这是编译器的限制,还是框架这部分设计者的想象力还是其他什么?

你可以从我对极其重要的事情的极度兴趣中得知,我有一段时间没有睡觉......

7 个答案:

答案 0 :(得分:11)

不,这些原因大多是错误的。它与“向后兼容性”或类似的东西无关。这不是因为有一个返回类型为Object[]的方法(在适当的情况下,许多签名被更改为泛型)。也不是因为获取数组会使其无法重新分配数组。他们并没有“错误地将其排除”或做出糟糕的设计决定。它们不包含T[] toArray(),因为它不能用数组的工作方式和类型擦除在泛型中的工作方式编写。

声明 List<T>方法签名T[] toArray()是完全合法的。但是,无法正确实现此类方法。 (你为什么不尝试练习?)

请记住:

  • 数组在运行时知道它们创建的组件类型。在运行时检查数组中的插入。并且在运行时检查从更通用的数组类型到更具体的数组类型的强制转换。要创建数组,您必须在运行时知道组件类型(使用new Foo[x]或使用Array.newInstance())。
  • 通用(参数化)类型的对象不知道它们创建的类型参数。类型参数被擦除到它们的擦除(下限),并且只在运行时检查它们。

因此,您无法创建类型参数组件类型的数组,即new T[...]

事实上,如果Lists有一个方法T[] toArray(),那么当前不可能创建通用数组(new T[n]):

List<T> temp = new ArrayList<T>();
for (int i = 0; i < n; i++)
    temp.add(null);
T[] result = temp.toArray();
// equivalent to: T[] result = new T[n];

泛型只是一个编译时的语法糖。可以通过更改一些声明并添加强制转换和填充来添加或删除泛型,而不会影响代码的实际实现逻辑。让我们比较1.4 API和1.5 API:

1.4 API:

Object[] toArray();
Object[] toArray(Object[] a);

在这里,我们只有一个List对象。第一种方法的声明返回类型为Object[]它创建了一个运行时类Object[]的对象。 (请记住,编译时(静态)类型的变量和运行时(动态)类型的对象是不同的东西。)

在第二种方法中,假设我们创建了一个String[]对象(即new String[0])并将其传递给它。数组具有基于其组件类型的子类型的子类型关系,因此String[]Object[]的子类,所以这是find。这里最重要的是它返回运行时类String[]的对象,即使它声明的返回类型是Object[]。 (同样,String[]Object[]的子类型,所以这并不罕见。)

但是,如果您尝试将第一个方法的结果转换为类型String[],则会得到一个类转换异常,因为如前所述,它的实际运行时类型为Object[]。如果您将第二种方法的结果(假设您传入String[])转换为String[],则会成功。

所以即使你可能没有注意到它(两种方法似乎都返回Object[]),这两种方法之间的预泛化中实际返回的对象已经存在很大的根本区别。

1.5 API:

Object[] toArray();
T[] toArray(T[] a);

完全同样的事情发生在这里。泛型添加了一些很好的东西,比如在编译时检查第二种方法的参数类型。但基本原理仍然相同:第一种方法创建一个实际运行时类型为Object[]的对象;第二种方法创建一个对象,其实际运行时类型与传入的数组相同。

事实上,如果你试图传入一个数组,其类实际上是T[]的子类型,比如U[],即使我们有一个List<T>,猜猜它会做什么?它会尝试将所有元素放入U[]数组(可能成功(如果所有元素都是U类型),或者失败(如果没有))返回一个实际类型的对象是U[]

早些时候回到我的观点。为什么不能制作方法T[] toArray()?因为您不知道要创建的数组类型(使用newArray.newInstance())。

T[] toArray() {
    // what would you put here?
}

为什么不能只创建new Object[n]然后将其投射到T[]?它不会立即崩溃(因为T在此方法中被删除),但是当你试图将它返回到外面时;并假设外部代码请求特定的阵列类型,例如String[] strings = myStringList.toArray();,它会引发异常,因为泛型中存在隐式转换。

人们可以尝试各种类型的黑客,比如查看列表的第一个元素来尝试确定组件类型,但这不起作用,因为(1)元素可以为null,(2)元素可以是实际组件类型的子类型,并且当您尝试将其他元素放入其中时,创建该类型的数组可能会失败。基本上,没有好办法解决此问题。

答案 1 :(得分:6)

toArray(String[])部分存在,因为在Java 1.5中引入泛型之前存在两种toArray方法。那时候,没有办法推断出类型参数,因为它们根本就不存在。

Java在向后兼容性方面非常重要,所以这就是为什么API的特定部分很笨拙。

整个类型擦除的东西也是为了保持向后兼容性。为1.4编译的代码可以愉快地与包含泛型的较新代码进行交互。

是的,它很笨拙,但至少它没有打破引入泛型时存在的巨大Java代码库。

编辑:因此,作为参考,1.4 API是这样的:

Object[] toArray();
Object[] toArray(Object[] a);

和1.5 API是这样的:

Object[] toArray();
T[] toArray(T[] a);

我不确定为什么可以更改1-arg版本的签名而不是0-arg版本。这似乎是一个合乎逻辑的变化,但也许在那里我有一些复杂的缺失。或许他们只是忘了。

EDIT2:在我看来,在这种情况下,Java 应该使用可用的推断类型参数,以及推断类型不可用的显式类型。但我怀疑实际包含在语言中会很棘手。

答案 2 :(得分:6)

其他人回答了“为什么”这个问题(我更喜欢Cameron Skinner的回答),我只想补充一点,你不会每次拥有来实例化一个新的数组而且它不一定是空。如果数组足够大以容纳集合,则它将用作返回值。因此:

String[] asArray = someStrings.toArray(new String[someStrings.size()])

只会分配一个正确大小的数组,并使用Collection中的元素填充它。

此外,一些Java集合实用程序库包括静态定义的空数组,可以安全地用于此目的。例如,请参阅Apache Commons ArrayUtils

编辑:

在上面的代码中,当Collection为空时,实例化的数组实际上是垃圾。由于数组不能在Java中调整大小,因此空数组可以是单例。因此,使用库中的常量空数组可能会稍微有效。

答案 3 :(得分:5)

那是因为类型擦除。请参阅维基百科关于Java泛型的文章中的Problems with type erasure:泛型类型信息仅在编译时可用,它被编译器完全剥离,并且在运行时不存在。

所以toArray需要另一种方法来确定要返回的数组类型。

维基百科文章中提供的示例非常具有说明性:

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

答案 4 :(得分:0)

根据Neal Gafter的说法,SUN根本没有像MS这样的资源。

SUN推出了类型擦除的Java 5,因为为运行时提供通用类型信息意味着更多的工作。他们不能再拖延了。

注意:向后兼容性不需要类型擦除 - 该部分由原始类型处理,这与可再现类型的概念不同。 Java仍然有机会删除类型擦除,即使所有类型都可以恢复;但是,任何可预见的议程都不够紧急。

答案 5 :(得分:0)

Java泛型实际上是不存在的东西。它只是一个语法(糖?或者说汉堡包?),它只由编译器处理。实际上它只是短(?)切换到类转换,所以如果你看一下字节码,你可能起初有点惊讶...... 类型擦除,如上文所述。

在集合上操作时,这种类转换的快捷方式似乎是个好主意。有一段时间,您至少可以声明要存储的元素类型,并且独立于程序员的机制(编译器)将检查该元素。但是,使用反射你仍然可以随意进入这样的集合:)

但是当你做泛型策略时,它适用于泛型bean,并且它被放入bean和策略的通用服务(?)中,为泛型​​bean等采用通用监听器。你将直接进入通用地狱。我已经完成了声明中指定的四个(!)泛型类型,当意识到我需要更多时,我决定取消整个代码的发生,因为我遇到了泛型类型合规性的问题。

至于菱形运算符 ...您可以跳过菱形并具有完全相同的效果,编译器将执行相同的检查并生成相同的代码。我怀疑在java的下一个版本中它会改变,因为需要这种向后兼容性......所以另一件事几乎没有给出任何东西,其中Java有更多的问题要处理,例如使用日期时间类型操作时极其不便......

答案 6 :(得分:-1)

我无法谈论JDK团队的设计决策,但Java泛型的许多笨拙性质来自于泛型是Java 5的一部分(1.5 - 无论如何)。 JDK中有很多方法,它们试图保持与1.5之前的API的向后兼容性。

至于繁琐的List<String> strings = new ArrayList<String>()语法,我打赌它是为了保持语言的迂腐本质。声明而非推断是Java行进顺序。

strings.toArray( new String[0] );?无法推断该类型,因为Array本身被视为泛型类(Array<String>,如果它被公开)。

总而言之,Java泛型主要用于保护您在编译时不输入错误。你仍然可以在运行时使用错误的声明来自己射击,并且语法很麻烦。正如经常发生的那样,使用仿制药的最佳实践是最好的。