Java15泛型指南中文版.docx
《Java15泛型指南中文版.docx》由会员分享,可在线阅读,更多相关《Java15泛型指南中文版.docx(31页珍藏版)》请在冰豆网上搜索。
Java15泛型指南中文版
Java1.5泛型指南中文版(Java1.5GenericTutorial)
1. 介绍
JDK1.5中引入了对java语言的多种扩展,泛型(generics)即其中之一。
这个教程的目标是向您介绍java的泛型(generic)。
你可能熟悉其他语言的泛型,最著名的是C++的模板(templates)。
如果这样,你很快就会看到两者的相似之处和重要差异。
如果你不熟悉相似的语法结构,那么更好,你可以从头开始而不需要忘记误解。
Generics允许对类型进行抽象(abstractovertypes)。
最常见的例子是集合类型(Containertypes),Collection的类树中任意一个即是。
下面是那种典型用法:
ListmyIntList=newArrayList();//1
myIntList.add(newInteger(0));//2
Integerx=(Integer)myIntList.iterator().next();//3
第3行的类型转换有些烦人。
通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。
但是,这个类型转换是必须的(essential)。
编译器只能保证iterator返回的是Object类型。
为了保证对Integer类型变量赋值的类型安全,必须进行类型转换。
当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(runtimeerror),因为程序员可能会犯错。
程序员如何才能明确表示他们的意图,把一个list(集合)中的内容限制为一个特定的数据类型呢?
这就是generics背后的核心思想。
这是上面程序片断的一个泛型版本:
ListmyIntList=newArrayList();//1
myIntList.add(newInteger(0));//2
Integerx=myIntList.iterator().next();//3
注意变量myIntList的类型声明。
它指定这不是一个任意的List,而是一个Integer的List,写作:
List。
我们说List是一个带一个类型参数的泛型接口(agenericinterfacethattakesatypeparameter),本例中,类型参数是Integer。
我们在创建这个List对象的时候也指定了一个类型参数。
另一个需要注意的是第3行没了类型转换。
现在,你可能认为我们已经成功地去掉了程序里的混乱。
我们用第1行的类型参数取代了第3行的类型转换。
然而,这里还有个很大的不同。
编译器现在能够在编译时检查程序的正确性。
当我们说myIntList被声明为List类型,这告诉我们无论何时何地使用myIntList变量,编译器保证其中的元素的正确的类型。
实际结果是,这可以增加可读性和稳定性(robustness),尤其在大型的程序中。
2. 定义简单的泛型
下面是从java.util包中的List接口和Iterator接口的定义中摘录的片断:
publicinterfaceList{
voidadd(Ex);
Iteratoriterator();
}
publicinterfaceIterator{
Enext();
booleanhasNext();
}
这些都应该是很熟悉的,除了尖括号中的部分,那是接口List和Iterator中的形式类型参数的声明(thedeclarationsoftheformaltypeparametersoftheinterfacesListandIterator)。
类型参数在整个类的声明中可用,几乎是所有可以使用其他普通类型的地方(但是有些重要的限制,请参考第7部分)。
(原文:
Typeparameterscanbeusedthroughoutthegenericdeclaration,prettymuchwhereyouwoulduseordinarytypes(thoughtherearesomeimportantrestrictions;seesection7))
在介绍那一节我们看到了对泛型类型声明List(thegenerictypedeclarationList)的调用,如List。
在这个调用中(通常称作一个参数化类型aparameterizedtype),所有出现的形式类型参数(formaltypeparameter,这里是E)都被替换成实体类型参数(actualtypeargument)(这里是Integer)。
你可能想象,List代表一个E被全部替换成Integer的版本:
publicinterfaceIntegerList{
voidadd(Integerx)
Iteratoriterator();
}
这种直觉可能有帮助,但是也可能导致误解。
它有帮助,因为List的声明确实有类似这种替换的方法。
它可能导致误解,因为泛型声明绝不会实际的被这样替换。
没有代码的多个拷贝,源码中没有、二进制代码中也没有;磁盘中没有,内存中也没有。
如果你是一个C++程序员,你会理解这是和C++模板的很大的区别。
一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。
类型参数就跟在方法或构造函数中普通的参数一样。
就像一个方法有形式参数(formalvalueparameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formaltypeparameters)。
当一个方法被调用,实参(actualarguments)替换形参,方法体被执行。
当一个泛型声明被调用,实际类型参数(actualtypearguments)取代形式类型参数。
一个命名的习惯:
我们推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。
最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。
许多容器类型使用E作为其中元素的类型,就像上面举的例子。
在后面的例子中还会有一些其他的命名习惯。
3. 泛型和子类继承
让我们测试一下我们对泛型的理解。
下面的代码片断合法么?
Listls=newArrayList();//1
List
第1行当然合法,但是这个问题的狡猾之处在于第2行。
这产生一个问题:
一个String的List是一个Object的List么?
大多数人的直觉是回答:
“当然!
”。
好,在看下面的几行:
lo.add(newObject());//3
Strings=ls.get(0);//4:
试图把Object赋值给String
这里,我们使用lo指向ls。
我们通过lo来访问ls,一个String的list。
我们可以插入任意对象进去。
结果是ls中保存的不再是String。
当我们试图从中取出元素的时候,会得到意外的结果。
java编译器当然会阻止这种情况的发生。
第2行会导致一个编译错误。
总之,如果Foo是Bar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G是G的子类型并不成立!
!
这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。
这种直觉的问题在于它假定这个集合不改变。
我们的直觉认为这些东西都不可改变。
举例来说,如果一个交通部(DMV)提供一个驾驶员里表给人口普查局,这似乎很合理。
我们想,一个List是一个List,假定Driver是Person的子类型。
实际上,我们传递的是一个驾驶员注册的拷贝。
然而,人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。
为了处理这种情况,考虑一些更灵活的泛型类型很有用。
到现在为止我们看到的规则限制比较大。
4. 通配符(Wildcards)
考虑写一个例程来打印一个集合(Collection)中的所有元素。
下面是在老的语言中你可能写的代码:
voidprintCollection(Collectionc){
Iteratori=c.iterator();
for(intk=0;k System.out.println(i.next());
}
}
下面是一个使用泛型的幼稚的尝试(使用了新的循环语法):
voidprintCollection(Collection
for(Objecte:
c){
System.out.println(e);
}
}
问题是新版本的用处比老版本小多了。
老版本的代码可以使用任何类型的collection作为参数,而新版本则只能使用Collection
那么什么是各种collections的父类呢?
它写作:
Collection
>(发音为:
"collectionofunknown"),就是,一个集合,它的元素类型可以匹配任何类型。
显然,它被称为通配符。
我们可以写:
voidprintCollection(Collection
>c){
for(Objecte:
c){
System.out.println(e);
}
}
现在,我们可以使用任何类型的collection来调用它。
注意,我们仍然可以读取c中的元素,其类型是Object。
这永远是安全的,因为不管collection的真实类型是什么,它包含的都是Object。
但是将任意元素加入到其中不是类型安全的:
Collection
>c=newArrayList();
c.add(newObject());//编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。
add方法有类型参数E作为集合的元素类型。
我们传给add的任何参数都必须是一个未知类型的子类。
因为我们不知道那是什么类型,所以我们无法传任何东西进去。
唯一的例外是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。
返回值是一个未知的类型,但是我们知道,它总是一个Object,因此把get的返回值赋值给一个Object类型的对象或者放在任何希望是Object类型的地方是安全的。
4.1. 有限制的通配符(BoundedWildcards)
考虑一个简单的画图程序,它可以用来画各种形状,比如矩形和圆形。
为了在程序中表示这些形状,你可以定义下面的类继承结构:
publicabstractclassShape{
publicabstractvoiddraw(Canvasc);
}
publicclassCircleextendsShape{
privateint x,y,radius;
publicvoiddraw(Canvasc){//...
}
}
publicclassRectangleextendsShape{
privateint x,y,width,height;
publicvoiddraw(Canvasc){
//...
}
}
这些类可以在一个画布(Canvas)上被画出来:
publicclassCanvas{
publicvoiddraw(Shapes){
s.draw(this);
}
}
所有的图形通常都有很多个形状。
假定它们用一个list来表示,Canvas里有一个方法来画出所有的形状会比较方便:
publicvoiddrawAll(Listshapes){
for(Shapes:
shapes){
s.draw(this);
}
}
现在,类型规则导致drawAll()只能使用Shape的list来调用。
它不能,比如说对List来调用。
这很不幸,因为这个方法所作的只是从这个list读取shape,因此它应该也能对List调用。
我们真正要的是这个方法能够接受一个任意种类的shape:
publicvoiddrawAll(List
extendsShape>shapes){//..}
这里有一处很小但是很重要的不同:
我们把类型List替换成了List
extendsShape>。
现在drawAll()可以接受任何Shape的子类的List,所以我们可以对List进行调用。
List
extendsShape>是有限制通配符的一个例子。
这里?
代表一个未知的类型,就像我们前面看到的通配符一样。
但是,在这里,我们知道这个未知的类型实际上是Shape的一个子类。
我们说Shape是这个通配符的上限(upperbound)。
像平常一样,要得到使用通配符的灵活性有些代价。
这个代价是,现在像shapes中写入是非法的。
比如下面的代码是不允许的:
publicvoidaddRectangle(List
extendsShape>shapes){
shapes.add(0,newRectangle());//compile-timeerror!
}
你应该能够指出为什么上面的代码是不允许的。
因为shapes.add的第二个参数类型是?
extendsShape——一个Shape未知的子类。
因此我们不知道这个类型是什么,我们不知道它是不是Rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个Rectangle不安全。
有限制的通配符正是我们解决DMV给人口普查局传送名单的例子所需要的。
我们的例子假定数据用一个姓名(String)到people(用Person或其子类来表示,比如Driver)。
Map是一个有两个类型参数的泛型类型的例子,表示map的键key和值value。
再一次,注意形式类型参数的命名习惯——K代表keys,V代表vlaues。
publicclassCensus{
publicstaticvoid addRegistry(MapextendsPerson>registry{
...
}
}
MapallDrivers=...;
Census.addRegistry(allDrivers);
5. 泛型方法
考虑写一个方法,它用一个Object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。
下面是第一次尝试:
staticvoidfromArrayToCollection(Object[]a,Collection
>c){
for(Objecto:
a){
c.add(o);//编译期错误
}
}
现在,你应该能够学会避免初学者试图使用Collection
或许你已经意识到使用Collection
>也不能工作。
回忆一下,你不能把对象放进一个未知类型的集合中去。
解决这个问题的办法是使用genericmethods。
就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
staticvoidfromArrayToCollection(T[]a,Collectionc){
for(To:
a){
c.add(o);//correct
}
}
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
Object[]oa=newObject[100];
Collection
fromArrayToCollection(oa,co);//T指Object
String[]sa=newString[100];
Collectioncs=newArrayList();
fromArrayToCollection(sa,cs);//TinferredtobeString
fromArrayToCollection(sa,co);//TinferredtobeObject
Integer[]ia=newInteger[100];
Float[]fa=newFloat[100];
Number[]na=newNumber[100];
Collectioncn=newArrayList();
fromArrayToCollection(ia,cn);//TinferredtobeNumber
fromArrayToCollection(fa,cn);//TinferredtobeNumber
fromArrayToCollection(na,cn);//TinferredtobeNumber
fromArrayToCollection(na,co);//TinferredtobeObject
fromArrayToCollection(na,cs);//compile-timeerror
注意,我们并没有传送真实类型参数(actualtypeargument)给一个泛型方法。
编译器根据实参为我们推断类型参数的值。
它通常推断出能使调用类型正确的最明确的类型参数(原文是:
Itwillgenerallyinferthemostspecifictypeargumentthatwillmakethecalltype-correct.)。
现在有一个问题:
我们应该什么时候使用泛型方法,又什么时候使用通配符类型呢?
为了理解答案,让我们先看看Collection库中的几个方法。
publicinterfaceCollection{
booleancontainsAll(Collection
>c);
booleanaddAll(Collection
extendsE>c);
}
我们也可以使用泛型方法来代替:
publicinterfaceCollection{
booleancontainsAll(Collectionc);
booleanaddAll(Collectionc);
// hey,typevariablescanhaveboundstoo!
}
但是,在containsAll和addAll中,类型参数T都只使用一次。
返回值的类型既不依赖于类型参数(typeparameter)也不依赖于方法的其他参数(这里,只有简单的一个参数)。
这告诉我们类型参数(typeargument)被用作多态(polymorphism),它唯一的效果是允许在不同的调用点,可以使用多种实参类型(actualargument)。
如果是这种情况,应该使用通配符。
通配符就是被设计用来支持灵活的子类化的,这是我们在这里要强调的。
泛型函数允许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。
如果没有这样的依赖关系,不应该使用泛型方法。
(原文:
Genericmethodsallowtypeparameterstobeusedtoexpressdependenciesamongthetypesofoneormoreargumentstoamethodand/oritsreturntype.Ifthereisn’tsuchadependency,agenericmethodshouldnotbeused.)
一前一后的同时使用泛型方法和通配符也是可能的。
下面是方法Collections.copy():
classCollections{
publicstatic voidcopy(Listdest,List
extendsT>src){...}
}
注意两个参数的类型的依赖关系。
任何被从源list从拷贝出来的对象必须能够将其指定为目标list(dest)的元素的类型——T类型。
因此源类型的元素类型可以是T的任意子类型,我们不关心具体的类型。
copy方法的签名使用一个类型参数表示了类型依赖,但是使用了一个通配符作为第二个参数的元素类型。
我们也可以用其他方式写这个函数的签名而根本不使用通配符:
classCollections{
publicstatic voidcopy(Listdest,Listsrc){...}
}
这也可以,但是第一个类型参数在dest的类型和第二个参数的类型参数S的上限这两个地方都有使用,而S本身只使用一次,在src的类型中——没有其他的依赖于它。
这意味着我们可以用通配符来代替S。
使用通配符比声明显式的类型参数更加清晰和准确,所以在可能的情况下使用通配符更好。
通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组。
这就有一个例子。
回到我们的画图问题,假定我们想要保持画图请求的历史记录。
我们可以把历史记录保存在Shape类的一个静态成员变量里,在drawAll()被调用的时候把传进来的参数保存进历史记录:
staticListextendsShape>>history=new