一 Generics简介
Generics 是程序设计语言的一种技术,指将程序中数据类型进行参数化,它本质上是对程序的数据类型进行一次抽象,扩展语言的表达能力,同时支持更大粒度的代码复用。 对于一些数据类型参数化的类和方法来说,它们往往具有更好的可读性、可复用性和可靠性。在设计集合类和它们的抽象操作时,往往需要将它们定义为与具体数据 类型无关,在这种情况下,使用Generics就是非常适合的。举例来说,如果我们需要设计一个Stack类,有时需要元素为int类型的Stack,有 时可能需要元素为Boolean或者Object类型的Stack。如果不使用Generics,我们通常需要定义不同的多个类,或者通过继承来实现。通 过继承实现往往引起数据类型的转换问题,本文稍后对其进行分析。如果使用Generics技术,将Stack的元素类型进行参数化,那么Stack类的只 需要实现一个版本,当需要某元素类型的Stack时,可以将类型作为参数来创建Stack对象。
二Generics与Java语言的发展
Generics 技术很早就被提出来了,但是Generics的实现和推广并非一帆风顺。这是因为Generics对语言规范的修改较大,对于编译技术要求也较高,另外, 90年代,所有语言都在积极扩展面向对象技术,并没有太多时间关注Generics。到目前为止,只少量语言支持Generics,例如Ada, C++, Eiffel等。在C++中, Template被用来现实Generics,同时C++还提供了标准模板库(STL,Standard Template Library)供开发人员使用,极大提高了编程的效率,因此Generics常常又被混称作Template。
但 是,到目前JDK1.4.1为止,Java语言不支持Generics。目前,对于数据类型的参数化,Java不能够直接实现,而是通常采用继承方式间接 实现。由于Java中,所有类都是Object的子类,因此如果需要支持多种类型时,我们可以将类型定义为Object,在使用的时候,可以通过父类和子 类进行相互转化来实现。以上面的Stack为例,我们可以将Stack的类型定义为Object类型,那么所有的Java对象都可以放在这个Stack 中,在存取对象时候,我们可以将对象强行转化成所需要的类型。但是,这种方式也会带来一些问题。
编译时,类型检查较为宽松,很多问题只有运行时候才能发现
很难实现复杂的类型参数化,缺少表达能力
增加使用者的麻烦,使用者需要对数据类型进行手工转换,并且进行类型检查
由于Java区分Object和基本类型,因此这种方式不容易支持基本类型。
虽 然当前Java语言规范并不支持Generics,但是一些组织和个人通过扩展Java语言的方式来实现Generics,其中比较著名的有GJ, PolyJ和NextGen。Java语言标准的制定组织Java Community Process(JCP)也早已收到关于在Java语言中支持Generics的建议,并且一直在讨论是否在Java语言支持Generics。其中,一 个比较重要的里程碑是Gilad Bracha博士等在2001年提出的提议。
Generics的实现对于Java语言本身、Class 文件格式和JVM构造都有较大的影响;另外,Generics在C++中的实现也存在一些问题,这些问题都应该在Java中尽量避免,最后,还有一部分人 认为Generics的引入会损失Java语言的简洁性。基于这些考虑,目前Java语言仍然不能够支持Generics,但是根据一些相关人员的预计, 2003年年底推出JDK1.5很可能要支持Generics。
三Java中Generics的使用
在这一章中,我们将预览一 些 Java的Generics的用法,虽然这些技术并没有正式推出,但是根据Java Generics提议的草稿版本来看,这些用法都比较稳定且成熟,预计它们与正式规范不会有太大差别。另外,这些Generics程序通过专用的编译器, 可以转换成兼容的Java类文件,并且可以在以前的JRE环境下运行,保证了向前兼容。本章节的部分内容来自Java语言Generics提议的草稿版 本,另外一些例子也参考了GJ的运行结果。
1) Generics的定义:
为了使用Generics,首先必须定义支持Generics的类,接口或者方法,它与C++语言的模板的语法类似。<>用于包含参数化类型,参数化类型用Java标识符标识表示,通常使用大写字母,例如T,A,B等。
1.1) 类和接口定义
以下是最简单的Generics类定义,定义了一个参数化类型T1:
以下是支持多个参数化类型的接口:
Java支持带有限制的参数化类型,这意味着在构造该类对象的时候,实际类型必须满足限制条件。在下面的例子中,T1的类型必须实现类Comparable接口,T2类型必须为Component类的子类,否则将构造失败。这些限制检查工作通常在编译的时候就可以进行。
复杂的定义可以带有限定的声明,甚至可以使用向前引用。
对于class的定义,基本与interface相同,此处不再详述。
1.2) 方法的定义
在方法中,通过定义参数化类型,可以提高方法的抽象级别,提高其可复用性。方法的参数化类型列表放在方法修饰符的后面,返回值的前面。在方法的参数中和方法体中,就可以直接使用参数化类型了。以下就是一个方法的例子。
带限定的Generics方法定义例子。
>
1.3) 实际例子
一但Java语言支持了Generics,Collection中的大部分类将用Generics方式重写,事实上,在一些支持Generics的Java扩展中,这些类已经被重写了。现在我们给出一个GJ中,使用Generics重写的Hashtable的定义。
与以前的Hashtable定义不同的是,它增加了两个用于表达Key和Value参数化类型(K,V),由于Dictionary和Map都将支持Generics,因此它们都使用Generics的表达方式。
2) Generics的使用:
2.1) 创建对象
Generics类在使用之前,必须按照定义进行初始化,设置参数化类型的实际类型,以下是构造Hashtable和Vector的一些例子。
2.2) Generics对象的类
在上例中,对于v1和v2对象来说,它们都是Vector类的对象,它们有相同的Class类型,换句话说,在运行时,以下表达式为真。虽然它们使用不同的参数类型创建,但是它们的Class类型是相同的。
Assert(V1.getClass().equals(v2.getClass()));
但是,如果我们定义一个 带有Vector<String>参数的方法,在调用该方法时,传入一个Vector<Integer>的对象,这将会导致编译失败。这是因为在编译时,这两个对象被当作不同的类型。
2.3) 类型检查
在 构造一个Generics对象时,编译器将首先检查参数化类型是否有效,例如是否满足限制条件等,确定所有参数化类型,然后编译器将在使用这些参数化类型 的地方进行类型检查,如果符合定义,那么编译通过,否则将编译失败,报告类型检查错误。因此,通过这种方式,编译器可以检查出很多类型不匹配的错误,避免 开发人员的错误,这也是Generics的重要优点之一。以下是类型检查的一些例子:
2.4) 类的强制转换
对 于Generics类的强制转化的原理,我们可以使用通用的转化规则进行操作。需要注意的一点是,同一个Generics类所定义的不同参数化类型的对象 之间是不能进行转化的。例如Vector<Object>和Vector<String>之间就是不能转化的。另外, Object也不能够转化成Generics类型,但是Generics类可以转化成Object。
以下是一些转化的例子
四 Generics的设计和实现
1) Java的Generics与C++的Template
由 于Java的Generics设计在C++的Template之后,因此Java的Generics设计吸取Template的很多经验和教训,特别是 Generics避免了一些Template已知的一些问题。首先,与Template不同的是,Generics的声明是需要进行类型检查的,而 Template不提供这一功能,这使得Generics的使用更加安全。另外,Java的Generics程序只需要编译一次,以后所有程序就可以复用 这个类字节码,而Template的实现是为每一个使用Template变量编译成一个新类,这会引起一定的冗余代码。
2) Generics的实现方案
Generics Java目前有很多不同的实现,比较著名的有GJ,PolyJ和NextGen。其中,GJ(Generic Java)是Gilad Bracha博士等设计和开发的支持Generics的Java编译器,它是较早,且较全面的Generics的解决方案,实际上,GJ是目前Java语 言的一个扩展,主要对编译器进行了扩展,以支持带有Generics 的Java 程序。
GJ的工作原理本质上就是消除程序中的 Generics语法,并将其转化成等价的无Generics的程序,这样编译的结果就是传统的类字节码,它们可以在传统的JVM中的运行,保证了向前兼 容。编译的过程就是将所有参数化类型的变量都替换为Object,通过这种方式消除所有的参数化类型。由于Java中,所有对象都可以转换程Object 的对象,因此通过这种方式可以消除参数化类型,但这种方法也有一个问题,那就是无法处理基本类型,因为Object与基本类型无法相互转换。在消除了参数 化类型后,在适当的地方还需要加上一些类型检查和转化语句。
例如Stack类的Generics源程序可能如下表示:
经过GJ改写后,程序将变为如下:
在使用Generics类的时候,我们首先设置参数化类型的值,在下例子我们传入Button类型。
GJ在编译以上代码时,在消除<>后,首先需要检查b是否能够转换成Button,如果b不能够转换成Button,编译器将报类型转化错误。同时, 在pop方法的返回值处,我们需要将Object类型显式转换为Button类型。以下就是GJ转化的结果;
以 上只描述了一些基本原理,在真正的实现中,GJ还需要处理继承,约束和类型转换等复杂问题。从实现的效果来看,GJ是非常成功地支持了Generics, 并且提供了通过Generics改写的java.utils.collection包。更多的实现技术,请参考相关资料。
在Android ,就会遇到较为复杂的使用,主要表现为约束和继承的联合使用,在Poolable 就使用到了返回类型:
<T extends Poolable<T>> Pool<T>