首页 > 学院 > 开发设计 > 正文

Java语言与Generics

2019-11-18 14:15:49
字体:
来源:转载
供稿:网友

内容:

一 Generics简介
二 Generics与java语言的发展
三 Java中Generics的使用
四 Generics的设计和实现
五 结论
参考资料
关于作者


在 Java 专区还有:

教学
工具与产品
代码与组件
所有文章
实用技巧




欧阳辰 (yeekee@sina.com)


一 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:
interface MyList<T1> {….. }

以下是支持多个参数化类型的接口:
interface MyList<T1,T2,T3> {….. }

Java支持带有限制的参数化类型,这意味着在构造该类对象的时候,实际类型必须满足限制条件。在下面的例子中,T1的类型必须实现类Comparable接口,T2类型必须为Component类的子类,否则将构造失败。这些限制检查工作通常在编译的时候就可以进行。
interface MyList<T1 implements Comparable, T2 extends Component> {}

复杂的定义可以带有限定的声明,甚至可以使用向前引用。
class Test<A implements ConvertibleTo<B>, B implements ConvertibleTo<A>{}

对于class的定义,基本与interface相同,此处不再详述。

1.2) 方法的定义
在方法中,通过定义参数化类型,可以提高方法的抽象级别,提高其可复用性。方法的参数化类型列表放在方法修饰符的后面,返回值的前面。在方法的参数中和方法体中,就可以直接使用参数化类型了。以下就是一个方法的例子。

public static <Elem> void swap(Elem[] a,int i,int j)
Elem temp=a[i];
a[i]=a[j]
a[j]=temp;
}





带限定的Generics方法定义例子。

public static <Elem implements Comparable> void swap(Elem[] a,int I,int j)
Elem temp=a[i];
a[i]=a[j]
a[j]=temp;
}





>
1.3) 实际例子
一但Java语言支持了Generics,Collection中的大部分类将用Generics方式重写,事实上,在一些支持Generics的Java扩展中,这些类已经被重写了。现在我们给出一个GJ中,使用Generics重写的Hashtable的定义。

public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, java.lang.Cloneable, java.io.Serializable {
public V put(K key, V value) {……}
…….
}





与以前的Hashtable定义不同的是,它增加了两个用于表达Key和Value参数化类型(K,V),由于Dictionary和Map都将支持Generics,因此它们都使用Generics的表达方式。

2) Generics的使用:

2.1) 创建对象
Generics类在使用之前,必须按照定义进行初始化,设置参数化类型的实际类型,以下是构造Hashtable和Vector的一些例子。

Hashtable<Integer,String> ht1=new Hashtable<Integer,String>();
Hashtable<Integer,String> ht2=null;
Vector<String> v1=new Vector<String>();
Vector<Integer> v2=new Vector<Integer>();






2.2) Generics对象的类
在上例中,对于v1和v2对象来说,它们都是Vector类的对象,它们有相同的Class类型,换句话说,在运行时,以下表达式为真。虽然它们使用不同的参数类型创建,但是它们的Class类型是相同的。
Assert(V1.getClass().equals(v2.getClass()));

但是,假如我们定义一个 带有Vector<String>参数的方法,在调用该方法时,传入一个Vector<Integer>的对象,这将会导致编译失败。这是因为在编译时,这两个对象被当作不同的类型。

void method(Vector<String> v) {};

Vector<Integer> v2=new Vector<Integer>();
method(v2);//编译失败






2.3) 类型检查
在构造一个Generics对象时,编译器将首先检查参数化类型是否有效,例如是否满足限制条件等,确定所有参数化类型,然后编译器将在使用这些参数化类型的地方进行类型检查,假如符合定义,那么编译通过,否则将编译失败,报告类型检查错误。因此,通过这种方式,编译器可以检查出很多类型不匹配的错误,避免开发人员的错误,这也是Generics的重要优点之一。以下是类型检查的一些例子:

Vector<String> strvector=new Vector<String>();
Vector<Integer> intvector=new Vector<Integer>();
strvector.add(new String());//OK
strvector.add(new Object());//编译失败,需要String类
strvector.add(new Integer());//编译失败,需要String类
intvector.add(new Integer());//OK
intvector.add(new String());//编译失败,需要Integer类






2.4) 类的强制转换
对于Generics类的强制转化的原理,我们可以使用通用的转化规则进行操作。需要注重的一点是,同一个Generics类所定义的不同参数化类型的对象之间是不能进行转化的。例如Vector<Object>和Vector<String>之间就是不能转化的。另外,Object也不能够转化成Generics类型,但是Generics类可以转化成Object。

以下是一些转化的例子

Class Dictionary<A,B> extends Object{}
Class Hashtable<A,B> extends Dictionary<A,B> {}
Dictionary<String,Integer> d=new Dictionary<String,Integer>();
Hashtable<String,Integer> h=new Hashtable <String,Integer>();
Hashtable<Float,Double> hfd=new Hashtable<Float,Double>();
Object o=new Object();
1) d= (Dictionary<String,Integer>)h//编译成功,运行成功;它们具有父子类关系。
2) h=(Hashtable<String,Integer>)d;// 编译成功,运行失败;它们具有父子类关系。
3) h=(Hashtable<String,Integer>)o;//编译失败,Object不能转化成Generics类;
4) hfd=(Hashtable<Float,Double>)d;//编译失败;






四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源程序可能如下表示:

public class Stack<A> {
public void push(A elem){…. }
public A pop(){…..}
}






经过GJ改写后,程序将变为如下:

public class Stack {
public void push(Object elem){….}
public Object pop(){….}
}






在使用Generics类的时候,我们首先设置参数化类型的值,在下例子我们传入Button类型。

Stack<Button> s = new Stack<Button>();
Button b = new Button("OK");
s.push(b);
b=s.pop();






GJ在编译以上代码时,在消除<>后,首先需要检查b是否能够转换成Button,假如b不能够转换成Button,编译器将报类型转化错误。同时, 在pop方法的返回值处,我们需要将Object类型显式转换为Button类型。以下就是GJ转化的结果;

Stack s = new Stack();
Button b = new Button("b");
s.push(b);
b=(Button)s.pop();






以上只描述了一些基本原理,在真正的实现中,GJ还需要处理继续,约束和类型转换等复杂问题。从实现的效果来看,GJ是非常成功地支持了Generics,并且提供了通过Generics改写的java.utils.collection包。更多的实现技术,请参考相关资料。

五 结论
Generics的出现将改变一些Java程序员的编程风格,以往所有不确定类型的对象都被定义为Object,需要使用对象时,通过强制类型转化获取,而Generics的出现将可用于治理一些类型抽象的类,让编译器来检查更多的类型匹配的问题,以减轻程序员的负担。但同时,Generics的引入,增加了Java程序的抽象程度,增加程序理解的难度。

相关资料:

Gilad Bracha, Norman Cohen,Christian Kemper etc, Adding Generics to the Java Programming Language Participant draft Specification, 2001,
http://java.sun.com/aboutJava/communityprocess/review/jsr014/
Paul Mingardi, Prepari8java.sun.com/developer/technicalArticles/releases/generics/">http://developer.java.sun.com/developer/technicalArticles/releases/generics/
Keith Turner, Catching more errors at compile time with Generic Java,IBM DeveloperWorks ,2001
http://www-106.ibm.com/developerworks/library/j-genjava.Html
Generic Java (GJ)
http://www.research.avayalabs.com/user/wadler/gj/


关于作者
欧阳辰,2001年毕业于北京大学计算机系,获硕士学位,SCJP(Sun Certificated Java Programmer),现任某公司软件工程师,长期从事java软件的研究和开发工作,已发表多篇Java相关的技术文章。联系方式yeekee@sina.com

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表