多角度看Java中的泛型.docx
- 文档编号:5731597
- 上传时间:2022-12-31
- 格式:DOCX
- 页数:17
- 大小:26.88KB
多角度看Java中的泛型.docx
《多角度看Java中的泛型.docx》由会员分享,可在线阅读,更多相关《多角度看Java中的泛型.docx(17页珍藏版)》请在冰豆网上搜索。
多角度看Java中的泛型
多角度看Java中的泛型
引言
非常多Java程式员都使用过集合(Collection),集合中元素的类型是多种多样的,例如,有些集合中的元素是Byte类型的,而有些则可能是String类型的,等等。
Java语言之所以支持这么多种类的集合,是因为他允许程式员构建一个元素类型为Object的Collection,所以其中的元素能是所有类型。
当使用Collection时,我们经常要做的一件事情就是要进行类型转换,当转换成所需的类型以后,再对他们进行处理。
非常明显,这种设计给编程人员带来了极大的不便,同时也容易引入错误。
在非常多Java应用中,上述情况非常普遍,为了解决这个问题,使Java语言变得更加安全好用,近些年的一些编译器对Java语言进行了扩充,使Java语言支持了"泛型",特别是Sun公司发布的JDK5.0更是将泛型作为其中一个重要的特性加以推广。
本文首先对泛型的基本概念和特点进行简单介绍,然后通过引入几个实例来讨论带有泛型的类,泛型中的子类型,及范化方法和受限类型参数等重要概念。
为了帮助读者更加深刻的理解并使用泛型,本文还介绍了泛型的转化,即,怎么将带有泛型的Java程式转化成一般的没有泛型的Java程式。
这样,读者对泛型的理解就不会仅仅局限在表面上了。
考虑到多数读者仅仅是使用泛型,因此本文并未介绍泛型在编译器中的具体实现。
Java中的泛型和C++中的模板表面上非常相似,但实际上二者还是有非常大差别的,本文最后简单介绍了Java中的泛型和C++模板的主要差别。
泛型概览
泛型本质上是提供类型的"类型参数",他们也被称为参数化类型(parameterizedtype)或参量多态(parametricpolymorphism)。
其实泛型思想并不是Java最先引入的,C++中的模板就是个运用泛型的例子。
GJ(GenericJava)是对Java语言的一种扩展,是一种带有参数化类型的Java语言。
用GJ编写的程式看起来和普通的Java程式基本相同,只不过多了一些参数化的类型同时少了一些类型转换。
实际上,这些GJ程式也是首先被转化成一般的不带泛型的Java程式后再进行处理的,编译器自动完成了从GenericJava到普通Java的翻译。
具体的转化过程大致分为以下几个部分:
o将参数化类型中的类型参数"擦除"(erasure)掉;
o将类型变量用"上限(upperbound)"取代,通常情况下这些上限是Object。
这里的类型变量是指实例域,本地方法域,方法参数及方法返回值中用来标记类型信息的"变量",例如:
实例域中的变量声明Aelem;,方法声明Node(Aelem){};,其中,A用来标记elem的类型,他就是类型变量。
o添加类型转换并插入"桥方法"(bridgemethod),以便覆盖(overridden)能正常的工作。
转化后的程式和没有引入泛型时程式员不得不手工完成转换的程式是非常一致的,具体的转化过程会在后面介绍。
GJ保持了和Java语言及Java虚拟机非常好的兼容性,下面对GJ的特点做一个简要的总结。
o类型安全。
泛型的一个主要目标就是提高Java程式的类型安全。
使用泛型能使编译器知道变量的类型限制,进而能在更高程度上验证类型假设。
如果没有泛型,那么类型的安全性主要由程式员来把握,这显然不如带有泛型的程式安全性高。
o消除强制类型转换。
泛型能消除原始码中的许多强制类型转换,这样能使代码更加可读,并减少出错的机会。
o向后兼容。
支持泛型的Java编译器(例如JDK5.0中的Javac)能用来编译经过泛型扩充的Java程式(GJ程式),不过现有的没有使用泛型扩充的Java程式仍然能用这些编译器来编译。
o层次清晰,恪守规范。
无论被编译的源程式是否使用泛型扩充,编译生成的字节码均可被虚拟机接受并执行。
也就是说不管编译器的输入是GJ程式,还是一般的Java程式,经过编译后的字节码都严格遵循《Java虚拟机规范》中对字节码的需求。
可见,泛型主要是在编译器层面实现的,他对于Java虚拟机是透明的。
o性能收益。
目前来讲,用GJ编写的代码和一般的Java代码在效率上是非常接近的。
不过由于泛型会给Java编译器和虚拟机带来更多的类型信息,因此利用这些信息对Java程式做进一步优化将成为可能。
以上是泛型的一些主要特点,下面通过几个相关的例子来对Java语言中的泛型进行说明。
带有泛型的类
为了帮助大家更好地理解Java语言中的泛型,我们在这里先来对比两段实现相同功能的GJ代码和Java代码。
通过观察他们的不同点来对Java中的泛型有个总体的把握,首先来分析一下不带泛型的Java代码,程式如下:
1interfaceCollection{
2publicvoidadd(Objectx);
3publicIteratoriterator();
4}
5
6interfaceIterator{
7publicObjectnext();
8publicbooleanhasNext();
9}
10
11classNoSuchElementExceptionextendsRuntimeException{}
12
13classLinkedListimplementsCollection{
14
15protectedclassNode{
16Objectelt;
17Nodenext=null;
18Node(Objectelt){this.elt=elt;}
19}
20
21protectedNodehead=null,tail=null;
22
23publicLinkedList(){}
24
25publicvoidadd(Objectelt){
26if(head==null){head=newNode(elt);tail=head;}
27else{tail.next=newNode(elt);tail=tail.next;}
28}
29
30publicIteratoriterator(){
31
32returnnewIterator(){
33protectedNodeptr=head;
34publicbooleanhasNext(){returnptr!
=null;}
35publicObjectnext(){
36if(ptr!
=null){
37Objectelt=ptr.elt;ptr=ptr.next;returnelt;
38}elsethrownewNoSuchElementException();
39}
40};
41}
42}
接口Collection提供了两个方法,即添加元素的方法add(Objectx),见第2行,及返回该Collection的Iterator实例的方法iterator(),见第3行。
Iterator接口也提供了两个方法,其一就是判断是否有下一个元素的方法hasNext(),见第8行,另外就是返回下一个元素的方法next(),见第7行。
LinkedList类是对接口Collection的实现,他是个含有一系列节点的链表,节点中的数据类型是Object,这样就能创建任意类型的节点了,比如Byte,String等等。
上面这段程式就是用没有泛型的传统的Java语言编写的代码。
接下来我们分析一下传统的Java语言是怎么使用这个类的。
代码如下:
1classTest{
2publicstaticvoidmain(String[]args){
3//bytelist
4LinkedListxs=newLinkedList();
5xs.add(newByte(0));xs.add(newByte
(1));
6Bytex=(Byte)xs.iterator().next();
7//stringlist
8LinkedListys=newLinkedList();
9ys.add("zero");ys.add("one");
10Stringy=(String)ys.iterator().next();
11//stringlistlist
12LinkedListzss=newLinkedList();
13zss.add(ys);
14Stringz=(String)((LinkedList)zss.iterator().next()).iterator().next();
15//stringlisttreatedasbytelist
16Bytew=(Byte)ys.iterator().next();//run-timeexception
17}
18}
从上面的程式我们能看出,当从一个链表中提取元素时需要进行类型转换,这些都要由程式员显式地完成。
如果我们不小心从String类型的链表中试图提取一个Byte型的元素,见第15到第16行的代码,那么这将会抛出一个运行时的异常。
请注意,上面这段程式能顺利地经过编译,不会产生所有编译时的错误,因为编译器并不做类型检查,这种检查是在运行时进行的。
不难发现,传统Java语言的这一缺陷推迟了发现程式中错误的时间,从软件工程的角度来看,这对软件的研发是非常不利的。
接下来,我们讨论一下怎么用GJ来实现同样功能的程式。
源程式如下:
1interfaceCollection{
2publicvoidadd(Ax);
3publicIteratoriterator();
4}
5
6interfaceIterator{
7publicAnext();
8publicbooleanhasNext();
9}
10
11classNoSuchElementExceptionextendsRuntimeException{}
12
13classLinkedListimplementsCollection{
14protectedclassNode{
15Aelt;
16Nodenext=null;
17Node(Aelt){this.elt=elt;}
18}
19
20protectedNodehead=null,tail=null;
21
22publicLinkedList(){}
23
24publicvoidadd(Aelt){
25if(head==null){head=newNode(elt);tail=head;}
26else{tail.next=newNode(elt);tail=tail.next;}
27}
28
29publicIteratoriterator(){
30returnnewIterator(){
31protectedNodeptr=head;
32publicbooleanhasNext(){returnptr!
=null;}
33publicAnext(){
34if(ptr!
=null){
35Aelt=ptr.elt;ptr=ptr.next;returnelt;
36}elsethrownewNoSuchElementException();
37}
38};
39}
40}
程式的功能并没有所有改动,只是在实现方式上使用了泛型技术。
我们注意到上面程式的接口和类均带有一个类型参数A,他被包含在一对尖括号(<>)中,见第1,6和13行,这种表示法遵循了C++中模板的表示习惯。
这部分程式和上面程式的主要差别就是在Collection,Iterator,或LinkedList出现的地方均用Collection,Iterator,或LinkedList来代替,当然,第22行对构造函数的声明除外。
下面再来分析一下在GJ中是怎么对这个类进行操作的,程式如下:
1classTest{
2publicstaticvoidmain(String[]args){
3//bytelist
4LinkedList
5xs.add(newByte(0));xs.add(newByte
(1));
6Bytex=xs.iterator().next();
7//stringlist
8LinkedList
9ys.add("zero");ys.add("one");
10Stringy=ys.iterator().next();
11//stringlistlist
12LinkedList
newLinkedList
13zss.add(ys);
14Stringz=zss.iterator().next().iterator().next();
15//stringlisttreatedasbytelist
16Bytew=ys.iterator().next();//compile-timeerror
17}
18}
在这里我们能看到,有了泛型以后,程式员并不必进行显式的类型转换,只要赋予一个参数化的类型即可,见第4,8和12行,这是非常方便的,同时也不会因为忘记进行类型转换而产生错误。
另外需要注意的就是当试图从一个字符串类型的链表里提取出一个元素,然后将他赋值给一个Byte型的变量时,见第16行,编译器将会在编译时报出错误,而不是由虚拟机在运行时报错,这是因为编译器会在编译时刻对GJ代码进行类型检查,此种机制有利于尽早地发现并改正错误。
类型参数的作用域是定义这个类型参数的整个类,不过不包括静态成员函数。
这是因为当访问同一个静态成员函数时,同一个类的不同实例可能有不同的类型参数,所以上述提到的那个作用域不应该包括这些静态函数,否则就会引起混乱。
泛型中的子类型
在Java语言中,我们能将某种类型的变量赋值给其父类型所对应的变量,例如,String是Object的子类型,因此,我们能将String类型的变量赋值给Object类型的变量,甚至能将String[]类型的变量(数组)赋值给Object[]类型的变量,即String[]是Object[]的子类型。
上述情形恐怕已深深地印在了广大读者的脑中,对于泛型来讲,上述情形有所变化,因此请广大读者务必引起注意。
为了说明这种不同,我们还是先来分析一个小例子,代码如下所示:
1List
2List
3lo.add(newInteger());
4Strings=ls.get(0);
上述代码的第二行将List
这里需要特别注意的是,这种赋值在泛型当中是不允许的!
List
如果上述赋值是合理的,那么上面代码的第三行的操作将是可行的,因为lo是List
读到此处,我们已看到了第二行的这种赋值所潜在的危险,他破坏了泛型所带来的类型安全性。
一般情况下,如果A是B的子类型,C是某个泛型的声明,那么C并不是C的子类型,我们也不能将C类型的变量赋值给C类型的变量。
这一点和我们以前接触的父子类型关系有非常大的出入,因此请读者务必引起注意。
泛化方法和受限类型参数
在这一部分我们将讨论有关泛化方法(genericmethod)和受限类型参数(boundedtypeparameter)的内容,这是泛型中的两个重要概念,还是先来分析一下和此相关的代码。
1interfaceComparable{
2publicintcompareTo(Athat);
3}
4
5classByteimplementsComparable
6privatebytevalue;
7publicByte(bytevalue){this.value=value;}
8publicbytebyteValue(){returnvalue;}
9publicintcompareTo(Bytethat){
10returnthis.value-that.value;
11}
12}
13
14classCollections{
15publicstatic
16Amax(Collectionxs){
17Iteratorxi=xs.iterator();
18Aw=xi.next();
19while(xi.hasNext()){
20Ax=xi.next();
21if(pareTo(x)<0)w=x;
22}
23returnw;
24}
25}
这里定义了一个接口Comparable,用来和A类型的对象进行比较。
类Byte实现了这个接口,并以他自己作为类型参数,因此,他们自己就能和自己进行比较了。
第14行到第25行的代码定义了一个类Collections,这个类包含一个静态方法max(Collectionxs),他用来在一个非空的Collection中寻找最大的元素并返回这个元素。
这个方法的两个特点就是他是个泛化方法并且有一个受限类型参数。
之所以说他是泛化了的方法,是因为这个方法能应用到非常多种类型上。
当要将一个方法声明为泛化方法时,我们只需要在这个方法的返回类型(A)之前加上一个类型参数(A),并用尖括号(<>)将他括起来。
这里的类型参数(A)是在方法被调用时自动实例化的。
例如,假设对象m的类型是Collection
Bytex=Collections.max(m);
调用方法max时,该方法的参数A将被推测为Byte。
根据上面讨论的内容,泛化方法max的完整声明应该是下面的形式:
max的方法体
}
不过,我们见到的max在中还多了"implementsComparable"一项,这是什么呢?
这就是我们下面将要谈到的"受限的类型参数"。
在上面的例子中,类型参数A就是个受限的的类型参数,因为他不是泛指所有类型,而是指那些自己和自己作比较的类型。
例如参数能被实例化为Byte,因为程式中有ByteimplementsComparable
这种限制(或说是范围)通过如下的方式表示,"类型参数implements接口",或是"类型参数extend类",上面程式中的"ByteimplementsComparable
泛型的转化
在前面的几部分内容当中,我们介绍了有关泛型的基础知识,到此读者对Java中的泛型技术应该有了一定的了解,接下来的这部分内容将讨论有关泛型的转化,即怎么将带有泛型的Java代码转化成一般的没有泛型Java代码。
其实在前面的部分里,我们或多或少地也提到了一些相关的内容,下面再来周详地介绍一下。
首先需要明确的一点是上面所讲的这种转化过程是由编译器(例如:
Javac)完成的,虚拟机并不负责完成这一任务。
当编译器对带有泛型的Java代码进行编译时,他会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种字节码能被一般的Java虚拟机接收并执行,这种技术被称为擦除(erasure)。
可见,编译器能在对源程式(带有泛型的Java代码)进行编译时使用泛型类型信息确保类型安全,对大量如果没有泛型就不会去验证的类型安全约束进行验证,同时在生成的字节码当中,将这些类型信息清除掉。
对于不同的情况,擦除技术所执行的"擦除"动作是不同的,主要分为以下几种情况:
o对于参数化类型,需要删除其中的类型参数,例如,LinkedList将被"擦除"为LinkedList;
o对于非参数化类型,不作擦除,或说用他自己来擦除自己,例如String将被"擦除"为String;
o对于类型变量(有关类型变量的说明请参考"泛型概览"相关内容),要用他们的上限来对他们进行替换。
多数情况下这些上限是Object,不过也有例外,后面的部分将会对此进行介绍。
除此之外,还需要注意的一点是,在某些情况下,擦除技术需要引入类型转换(cast),这些情况主要包括:
情况1.方法的返回类型是类型参数;
情况2.在访问数据域时,域的类型是个类型参数。
例如在本文"带有泛型的类"一小节的最后,我们给出了一段测试程式,一个Test类。
这个类包含以下几行代码:
8LinkedList
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 角度 Java 中的
![提示](https://static.bdocx.com/images/bang_tan.gif)