Java泛型擦除机制之答疑解惑
Puzzling Through Erasure: answer section
Author: Neal Gafter (One of the Lead developers for Java SE5)
翻译:Ray (C) 版权所有 保留所有权利
Translated by Ray, all rights reserved.
写在前面
原文(或博客/blog)由Neal Gafter在2004年所写,他是Java SE5的主要开发人员之一。在博客中,他阐释了为什么Java 5为什么不直接像微软的C#语言那样支持reification(具体化)的泛型(generics),而设计了擦除机制(erasure),并就一些问题展开了讨论。读罢原文,我对Java中泛型的设计有了总体性的理解。此文翻译自原作者Neal Gafter的个人博客,大家可以点击上面的副标题查看原文,如没法打开链接,那请翻墙访问他的个人博客地址http://gafter.blogspot.com/2004/09/puzzling-through-erasure-answer.html
擦除机制(erasure)是Java5用来实现泛型的技术。一般来说,在运行时阶段,Java编译器先执行类型检查,然后执行擦除或删除泛型信息。而具体化(reification)的泛型,与此正好相反。基于具体化泛型系统的类实现在运行时作为顶级实体,它在运行时保留了类型参数,而这给基于类型和反射性的语言提供了确定的操作。Sun公司选择采用擦除机制实现泛型,而C#实现了具体化的 泛型。在新版的Java语言发行的背景下,人们对擦除机制和底层实现泛型的两种不同的方式提出了许多问题,而误解也随着产生。我希望借这篇博文来阐明这些问题。
为什么Sun公司选择采用擦除机制实现泛型,而不是像C#那样从底层具化类型参数?这个问题的提出,这可能是Java泛型设计最没被理解的地方,但这就是对Java言语扩展的设计目标的自然而然的结果。这种设计是为向后兼容性提供源代码和目标代码两方面的支持。向后兼容性的意义是很明显的:希望现有的代码和类文件在新版本的Java中继续使用,而不发生冲突。虽然向后兼容性的定义已经很清楚,但是在没有破会Java语言的同时更改语言本省是很困难的一件事情。
另一个制约设计的原因是移植兼容性,而且这是个大问题。我待会儿用例子来解释。
移植兼容性
移植兼容性是“jsr14 Java语言扩展泛型设计”需求中的第一个约束:
约束1:与现有代码向上兼容。以前的代码必须能在新系统中运行。这就是说,不仅类文件格式要向上兼容,而且使用以前写的参数化版本库的老应用程序也要向上兼容,特别是是基于Java平台的标准库编写的应用。
想象一下,有两个软件供应商:A公司销售用于操作音频的Java类库,B公司销售用来控制电子喇叭的Java类库(我想这个产品应当叫做Boogie Woogie Boy)。C公司销售电子喇叭,而这个电子喇叭的开发是基于A公司和B工资的类库。通常,C公司的客户都同时具有三家公司产品的license。
随着JDK5的发布,这些厂商一般都想迁移到支持泛型的Java版本。对于公司A和公司B来说,它们都处于软件栈的底层,属于相对有利位置,可以利用Java平台的新特性快速开发出新版本的软件接口。作为新发布的产品,公司A自然会新增一些新的功能。A公司的新API叫A2。A公司的用户,因为没有修改老的源文件来获得使用泛型带来的好处,他们仍可以获得A的license。
类似地,B公司也发行B和B2两个软件版本。
那么这对C公司意味着什么?首先,他们不能迁移到新版本,直到所有新的类库都能得到。新的类库到齐后,C公司要移植到新版本,其原因有二:第一,利用改善的语言特性来提高软件开发过程。第二,公司的未来用户将会得到新的(而不是老的)A2和B2的license。因此,C公司提供两个版本的产品。基于类库A和B的老版本,和基于A2和B2类库的新版本。
C2版本发行不久(侵犯了可口可乐【Cola Cola】=‘C2’公司的商标权而遭到起诉,呵呵),C公司意识到它忽视了许多用户,这些用户持有新版本类库A和类库B的license。因此C公司匆匆发布C2a和C2b两个新版本给这一用户群体。
当我们推进软件栈时,事情只会变得更糟。第一,在每个用户都完全移植到新的API前,将会经历很漫长的一段时间。第二,作为类库的开发人员,你要么让用户使用最新的API,要么为无数各种底层的变异版本提供支持。如果你有足够的远见,能控制真个软件栈,那么这就不是什么大问题——至少对用户来说。但这对Sun公司的情况来说,这是不允许的。
这就是移植兼容性的由来。移植兼容性要求API的泛型版本要和老的版本兼容,就是说用户能继续编译(源文件)和运行(编译后的程序)。这种要求在很大程度上限制了Java泛型设计的空间。
在支持兼容性移植的体系下,A公司和B公司可以互不影响地采用泛型机制重写类库,并且这两家公司的用户在不变更源码的条件下,甚至不用再次编译源代码,而使用新的类库。现在,A公司和B公司都能发行单一版本的开发库。
我们找不到在系统中既要支持具象泛型的同时又要满足兼容性移植的约束,亦即,在运行时访问类型参数(type parameter)。我们并不是没努力去寻找这样的方法。事实上,我们已经对设计空间有了足够的探索,可以说,没有简单的方法能达到目的。探索过程的细节非常有意思,但这超出了我们现在讨论的范围。Bruce Eckel(译者注:著名的《Thinking In Java》一书的作者)的注入思想通常认为是探索起点,叫做NextGen,这种思想也不支持兼容性移植。
蛋疼的向后兼容性
我在前面提过,虽然词语“向后兼容性”要比移植兼容性容易理解。然而,在不破坏兼容性的前提下,来扩展语言是比较困难的。例如,用Bruce的话来说,Java的未来版本将采用具体的类型参数取缔擦除机制。他的思想是,泛型类的构造器调用,将传递一个或多个额外的参数——实例化类型参数信息——将会存储在实例中。
我们先撇开实现的复杂性和由此带来的性能问题不说(你真的想要每个链接列表LinkedList节点体积加倍?怎样给instanceof实现泛型接口?怎样实现数组存储检查?),这真的不是向后兼容了。基本问题是现存Java5 class文件不遵循那个规则,并且不会和新的代码协同工作。实现了Foo
我想解释Bruce在他最近博客中曲解的几个概念,点这和这查看。
为什么不能使用“newT()”?
对于类型参数T,不能“new T()”是因为,并不是每个类型都有一个空的构造器。我知道Bruce是“latent typing”的铁杆粉丝,他乐意在运行时得到这样的错误。但是,Java是静态类型语言,从好的或坏的方面来说,我们不能简单的调用某一类型的空构造器,因为这个类型自身并不静态地知道有这样的构造器存在。由于擦除机制,所以没有办法来生成这种代码,这也没错,但对这种构造方式来说,这已经偏离了论点。
如果擦除了类型参数(type parameters)将是什么情况?
静态类型检查!静态类型检查!静态类型检查!重要的事情说三遍。如果你更倾向于Bruce所谓的“latent typing”,那么你更注重运行时而非编译时。如果是这样的话,对静态类型系统的任何扩展都没有激动人心的地方,那就没必要浪费过多的脑细胞在上面。
但对依赖于编译器在早期来帮助静态校正程序和提前找到错误的人来说,泛型系统是优胜者。对基于具体类型来执行时需要推测未转化的集合的类型的程序,泛型将节约不少脑力。
怎样才能创建类型T的数组?
有两种方式——1:捷径。虽然不是类型安全,但可以在多数情况下运行良好2:正确的方式。Bruce找到了捷径的方式,不幸的是,这已经广泛地成为了“正确”的方式。我忐忑着传递错误信息的风险,贴上代码吧:
T[] a = (T[]) new Object[N]; // Bad style! 在代码中写上如上语句,编译器在编译时会出现警告。毕竟,Object数组不是具体T类型的数组,除非T刚好是Object类型的。我们不应该忽视这种编译警告。事实上,这种写法是非常危险的。如果在API中有一个方法method返回T[],并且有一个客户化代码T==String,将返回值存入String[]类型的一个变量中,那么在运行时将得到ClssCastException错误。
你会说,等等,这种事情出现在Collections的实现中!那么,这种写法是允许的,可接受的,合适的方法。不,你错了,其实上是我很懒。在collection中使用Object数组才是更好的方式,并且在数组中移除(remove)一个元素时,在任何需要的地方都添加cast转换(没错,转换成具体T类型)。总的来说,这样的转换由javac工具完成,最后移动到非泛型化的客户化代码中,两者都由擦除机制的方式编译。但在每种转换的过程中,依然会得到编译警告。这样的警告是没法避免的,因为在运行时,客户化代码不提供足够的类型信息来进行正确的转换。这不是擦除机制的问题,而是Java语言演化所带来的问题。
Neal Gafter“懒”的证据。(Brauce Eckelz在《Thinking In Java》中写道):
但那不是正确的解决问题的方案。正确的方案需要在设计API时意识到泛型(然而不幸的是,不可能在现有的代码上重写)和依赖使用type token(类型标识)。如果真的不想让类型参数被擦除,那么可以用以下代码达到目的:
Bruce Eckel的原文:
现在,我们可以利用反射(tClass.newInstance())来实例化,创建数组(Array.newInstance),cast转换(Class.cast),还可以用instanceof做类型测试(Class.isInstance)。这或许不是你所喜欢的语法风格。
这种技术可以达到Bruce所谓的“类型注入”的效果,这几乎取消了类型擦除机制。我为什么说“几乎”?留给你们去猜吧!提示一下:如你所惧,它与擦除机制有关。
为什么不能再实例化时指定范围限定(bounds)?
这是什么鬼?如果能new一个List<?Extends Foo>(),那么可以将它放入Foo或任何Foo的子类中。但是Foo的子类还是Foo。因此,直接用List
换种方法来思考,想象一下方法(method)参数。我们可以在方法值参数中放类型,其中,参数名已被定义,但它不是方法调用时的参数,因为方法调用时的参数是表达式。同样,这对类型参数(type parameters)和参数(arguments)也适用:可以在类型参数上设定范围限定,其name也定义好,但不能给参数设定范围(bounds)。
为什么你认为C#的泛型系统破坏了兼容性?
简单来说:我不这么认为。
微软从本质上废弃了老版本的集合(collections)类,引进了一组新的泛型集合类。微软在制造我们所描述的A-B-C公司的问题,只是对微软来说,这种问题不是什么大的问题,因为他们对整个的软件栈控制得太多了。我并不是说,他们破坏了向后兼容性。实际上,如果Java泛型的设计目标没有和具体化(reification)的泛型设计冲突的话,我更倾向具象(具体化)的设计方法。在C#泛型设计中有一个不好的地方是(除了抛弃移植兼容性外),很难将两种类型系统和通配符整合在一起,虽然这个问题是可以得到解决的。
或许你不赞同移植兼容性作为Java泛型扩展的目标。这种反馈在1999年jsr14形成时会很有用,其实,在那个时候,这个问题已经讨论了很长时间。无论你们怎么认为,我希望你们现在能理解,在产品发布前一个月再来考虑根本的设计目标是不现实的,现在(2004)的产品和设计深深地依赖于5年前的根本目标。
最后来个反驳Neal Gafter的,激烈点的: