【Java精品源码栏目提醒】:网学会员鉴于大家对Java精品源码十分关注,论文会员在此为大家搜集整理了“Java泛型编程指南 - 软件工程”一文,供大家参考学习
Java 泛型编程指南此 系 列 文 章 译 自 SUN 的 泛 型 编 程 指 南 看 不 懂 译 文 的 请 看 原 文http://
java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf要点: 1.泛型,故名思意,可以“莫须有”地构造类型。
2.类型本身有继承关系,使用集合泛型不存在破坏类型存防规则, 应使用 代替 3.参数类型可有上下限制,并可设置多种类型amp。
4.泛型方法好处:消除类型转换,实现的是quot面向接口编程quot扩展eg amp extends super。
一、绪言 JDK1.5 对
JAVA 语言进行了做了几个扩展,其中一个就是泛型。
本指南旨在介绍泛型。
如果你熟悉其它语言的构造类似的东西,特别是 C的模板(template),你会很快发现它们之间的相同点及重要的不同点;如果你在其他地方没看到过类似的东西,那反而更好,那样你就可以开始全新的学习,用不着去忘掉那些(对
JAVA 泛型)容易产生误解的东西。
泛型允许你对类型进行抽象。
最常见的例子是容器类型,比如那些在 Collection层次下的类型。
下面是那类例子的典型用法:List myIntList new LinkedList//1myIntList.addnew Integer0 //2Integer x Integer myIntList.iterator.next//3 第 3 行里的强制类型转换有点烦人,程序通常都知道一个特定的链表(List)里存放的是何种类型的数据,但却一定要进行类型转换。
编译器只能保证迭代器返回的是一个对象,要保证对 Integer 类型变量的赋值是类型安全的话,必须进行类型转换。
类型转换不但会引起程序的混乱,还可能会导致运行时错误,因为程序员可能会犯错误。
如果程序员可以如实地表达他们的意图,即标记一个只能包含特定数据类型的链表,那会怎么样呢?这就是泛型背后的核心思想。
下面是前面代码的泛型写法:ListltIntegergt myIntList new LinkedListltIntegergt//1myIntList.addnew Integer0 //2Integer x myIntList.iterator.next//3 请注意变量 myIntList 的类型声明,它指明了这不仅仅是一个任意的 List,还是一个 Integer 类型的 List,写作 ListltIntegergt。
我们说 List 是一个接受类型(在这个例子是 Integer)参数的泛化的接口,在创建链表对象的时候,我们也指定了一个类型参数。
另外要注意的是在第 3行的类型转换已经不见了。
现在你可能会想,我们所做的全部都是为了把混乱消除。
我们没有在第 3 行把类型转换为 Integer,而是在第 1行加了 Integer 类型参数;非也非也,这里面差别很大,编译器现在能够在编译期间检测程序的类型正确性。
当我们把myIntList 声明为类型 ListltIntegergt的后,就意味着变量 myIntList 在何时何地的使用都是正确的,编译器保证了这一点。
相反,类型转换只是告诉我们程序员认为它在程序的某个地方是正确的。
实际的结果是,程序(特别是大型的程序)的可读性和健壮性得到了提高。
二、定义简单的泛型下面是
java.util 包里的 List 和 Iterator 接口定义的一个小小的引用:public interface ListltEgt void addE x IteratorltEgt iteratorpublic interface IteratorltEgt E next boolean hasNext 除了尖括号里的东西,这里所有的都应该很熟悉了。
那是 List 和 Iterator接口的规范类型参数的声明。
类型参数可以用在任何的泛型声明中,就像使用普通的类型一样(虽然有一些很重要的限制;看第 7 部分)。
在绪言中,我们看到了 List 泛型声明的调用,比如 ListltIntegergt。
在调用里面(通常称为参数化类型),所有出现规范类型参数(这里是 E)的全部都用实际的类型参数(这里是 Integer)所代替。
你可以想象成 ListltIntegergt代表所有 E 都用 Integer 代替了的 List:public interface IntegerList void addInteger x IteratorltIntegergt iterator这种想法是有所帮助的,但也会造成误解。
它是有所帮助的,是因为参数化类型 Listltintegergt有看起来像这种扩展的方法。
它会造成误解,是因为泛型的声明实际上不会像那样去扩展;在源代码中、二进制文件中、硬盘和内存里,都没有代码的多个拷贝。
如果你是一个 C程序员,你会明白这跟 C的模板(template)很不同。
泛型声明是一次编译,永远使用,它会变成一个单独的 class 文件,就像一个普通的类或接口声明。
类型参数跟用在方法或构造函数里的普通的参数类似,就像一个方法具有描述它运算用到的值的类型的规范值参一样,泛化声明具有规范类型参数。
当一个方法被调用的时候,实际的参数将会被规范参数所代替而对方法求值。
当一个泛化声明被调用的时候,实际类型参数将会代替规范类型参数。
命名惯例要注意的一个地方。
我们建议你用一些简炼(如果可以的话只用一个字符)但却映眼的名字作为规范类型参数名。
在那些名字中最好避免小写字母,这样可以很容易把规范类型参数和普通的类或接口区分开来。
就像前面的例子一样,很多容器类型使用 E。
我们将会在后面的例子里看到其他的惯例。
三、泛型和子类化我们来测试一下对泛型的理解,下面的代码是否正确呢?ListltStringgt ls new ArrayListltStringgt//1ListltObjectgt lo ls//2第 1 行肯定是正确的,问题的难点在于第 2 行;这样就归结为这个问题:一个字符串(String)链表(List)是不是一个对象链表?大部分人的直觉是:“肯定 ”了!。
那好,看一下下面这两行:lo.addnew Object//3String s ls.get0//4:企图把一个对象赋值给字符串! 在这里我们把 ls 和 lo 搞混淆了。
我们通过别名 lo 来访问字符串链表 ls,插入不确定对象;结果就是 ls 不再存储字符串,当我们尝试从里面取出数据的时候就会出错。
Java 编译器当然不允许这样的事情发生了,所以第 2 行肯定会编译出错。
一般来说,如果 Foo 是 Bar 的子类型(子类或子接口),而 G 又是某个泛型声明的话,GltFoogt并不是 GltBargt的子类型。
这可能是学习泛型的时候最难的地方,因为它与我们的深层直觉相违背。
直觉出错的问题在于它把集合里的东西假想为不会改变的,我们的本能把这些东西看作是不变的。
举个例子,假设汽车公司为人口调查局提供一份驾驶员的列表,这看上去挺合理。
假设 Driver 是 Person 的一个子类,则我们认为 ListltDrivergt是一个ListltPersongt。
而实际上提交的是一份驾驶员登记表的一个副本。
否则的话,人口调查局将可以驾驶员的人加入到那份列表中去,汽车公司的纪录受到破坏。
为了解决这类问题,我们需要考虑一些更灵活的泛型,到现在为止碰到的规则太受约束了。
四、通配符考虑一下写一个程序来打印一个集合对象(collection)里的所有元素。
在旧版的语言里面,你可以会像下面那样写:void printCollectionCollection c Iterator i c.iterator for k 0 k lt c.size k System.out.printlni.next 下面尝试着用泛型(和新的 for 循环语法)来写:void printCollectionCollectionltObjectgt c for Object e : c System.out.printlne 这样的问题是新版本的代码还没旧版本的代码好用。
就像我们刚示范的一样,CollectionltObjectgt 并 不 是 所 有 类 型 的 集 合 的 父 类 型 , 所 以 它 只 能 接 受CollectionltObjectgt对象,而旧版的代码却可以把任何类型的集合对象作为参数来调用。
那么,什么才是所有集合类型的父类型呢?这个东西写作 Collectionltgt(读 )作“未知集合”,就是元素类型可以为任何类型的集合。
这就是它为什么被称为“通配符类型”的原因。
我们可以这样写:void printCollectionCollectionltgt c for Object e : c System.out.printlne 现在,我们就可以以任何类型的集合对象作为参数来调用了。
注意,在printCollection方法里面,我们仍然可以从 c 对象中读取元素并赋予 Object类型;因为无论集合里实际包含了什么类型,它肯定是对象,所以是类型安全的。
但对它插入任意的对象的话则是不安全的:Collectionltgt c new ArrayListltStringgtc.addnew Object//编译错误 由于我们并不知道 c 的元素类型是什么,因此我们不能对其插入对象。
add方法接受类型 E,即集合的元素类型的参数。
当实际的类型参数是的时候,就代表是某未知类型。
任何传递给 add 方法的参数,其类型必须是该未知类型的子类型。
因为我们并不知道那是什么类型,所以我们传递不了任何参数。
唯一的例外就是 null,因为它是任何(对象)类型的成员。
另外,假设有一个 Listltgt,我们可以调用 get方法并使用其返回结果。
结果类型是一个未知类型,但我们都知道它是一个对象。
因此把 get方法的返回结果赋值给对象类型,或者把它作为一个对象参数传递都是类型安全的。
四、1-有界通配符考虑一个简单的画图程序,它可以画长方形和圆等形状。
为了表示这些形状,你可能会定义这样的一个类层次结构:public abstract class Shape public abstract void drawCanvas cpublic class Circle extends Shape private int x y radius public void drawCanvas c ... public class Rectangle extends Shape private int x y width height public void drawCanvas c ... 这些类可以在 canvas 上描画:public class Canvas public void drawShape s s.drawthis 任何的描画通常都包括有几种形状,假设它们用一个链表来表示,那么如果在 Canvas 里面有一个方法来画出所有的形状的话,那将会很方便:public void drawAllListltShapegt shapes for Shape s: shapes s.drawthis 但是现在,类型的规则说 drawAll方法只能对确切的 Shape 类型链表调用,比如,它不能对 ListltCirclegt类型调用该方法。
那真是不幸,因为这个方法所要做的就是从链表中读取形状对象,从而对 ListltCirclegt类型对象进行调用。
我们真正所想的是要让这个方法能够接受一个任何形状的类型链表:public void drawAllListlt extends Shapegt shapes ... 这里有一个很小但很重要的不同点:我们把类型 ListltShapegt替换为Listlt extends Shapegt。
现在 drawAll方法可以接受任何 Shape 子类的链表,我们就可以如愿的对ListltCirclegt调用进行啦。
Listlt extends Shapegt是一个有界通配符的例子。
表示一个未知类型,就像我们之前所看到的通配符一样。
但是,我们知道在这个例子里面这个未知类型实际是 Shape 的子类型(注:它可以是 Shape 本身,或者是它的子类,无须在字面上表明它是继承 Shape 类的)。
我们说 Shape 是通配符的“上界”。
如往常一样,使用通配符带来的灵活性得要付出一定的代价;代码就是现在在方法里面不能对 Shape 对象插入元素。
例如,下面的写法是不允许的:public void addRectangleListlt extends Shapegt shapes shapes.add0 new Rectangle //编译错误 你应该可以指出为什么上面的代码是不允许的。
shapes.add方法的第二个参数的类型是 继承 Shape,也就是一个未知的 Shape 的子类型。
既然我们不知道类型是什么,那么我们就不知道它是否是 Rectangle 的父类型了;它可能是也可能不是一个父类型,因此在那里传递一个 Rectangle 的对象是不安全的。
有界通配符正是需要用来处理汽车公司给人口调查局提交数据的例子方法。
在我们的例子里面,我们假设数据表示为姓名(用字符串表示)对人(表示为引用类型,比如 Person 或它的子类型 Driver 等)的映射。
MapltK Vgt是有两个类型参数的一个泛型的例子,表示键值映射。
请再一次注意规范类型参数的命名惯例:K 表示键,V 表示值。
public class Census public static void addRegistryMapltString extends Persongt registry … ...MapltString Drivergt allDrivers ...Census.addRegistryallDrivers五、泛型方法 考虑写这样一个方法,它接收一个数组和一个集合(collection)作为参数,并把数组里的所有对象放到集合里面。
先试试这样:static void fromArrayToCollectionObject a Collectionltgt c for Object o : a c.addo//编译错误 到现在,你应该学会了避免把 CollectionltObjectgt作为集合参数的类型这种初学者的错误;你可能或可能没看出使用 Collectionltgt也是不行的,回想一下,你是不能把对象硬塞进一个未知类型的集合里面的。
解决这类问题的方法是使用泛型方法。
就像类型声明一样,方法也可以声明为泛型的,就是说,用一个或多个类型参数作为参数。
static ltTgt void fromArrayToCollectionTa CollectionltTgt c for T o : a c.addo//正确 对于集合元素的类型是数组类型的父类型,我们就可以调用这个方法。
Object oa new Object100CollectionltObjectgt co new ArrayListltObjectgtfromArrayToCollectionoa co// T 是对象类型String sa new String100CollectionltStringgt cs new ArrayListltStringgtfromArrayToCollectionsa cs// T 是字符串类型(String)fromArrayToCollectionsa co// T 对象类型Integer ia new Integer100Float fa new Float100Number na new Number100CollectionltNumbergt cn new ArrayListltNumbergtfromArrayToCollectionia cn// T 是 Number 类型fromArrayToCollectionfa cn// T 是 Number 类型fromArrayToCollectionna cn// T 是 Number 类型fromArrayToCollectionna co// T 是 Number 类型fromArrayToCollectionna cs// 编译错误请注意,我们并没有把实际的类型实参传递给泛型方法,因为编译器会根据实参的类型为我们推断出类型实参。
一般地,编译器推断得到可以正确调用的最接近的(the most specific)实参类型。
现在有一个问题:我应该什么时候使用泛型方法,什么时候使用通配符类型呢?为了明白这个问题的答案,我们来看看 Collection 库里的几个方法:interface CollectionltEgt public boolean containsAllCollectionltgt c public boolean addAllCollectionlt extends Egt c在这里我们也可以用泛型方法:interface CollectionltEgt public ltTgt boolean containsAllCollectionltTgt c public lt extends Egtboolean addAllCollectionltTgt c //哈哈,类型变量也可以有界! 但是,类型参数 T 在 containsAll 和 addAll 两个方法里面都只是用了一次。
返回类型并不依赖于类型参数或其他传递给该方法的实参(这种是只有一个实参的简单情况)。
这就告诉我们类型实参是用于多态的,它的作用只是对不同的调用可以有一系列的实际的实参类型。
如果是那样的话,就应该使用通配符,通配符就是设计来支持灵活的子类型的,这也是我们这里所要表述的东西。
泛型方法允许类型参数用于表述一个或多个的实参类型对方法或及其返回类型的依赖关系。
如果没有那样的一个依赖关系的话,泛型方法就不应用使用。
也有可能是一前一后一起使用泛型方法和通配符的情况,下面是Collections.copy方法:class Collections public static ltTgt void copyListltTgt dest listlt extends Tgt src ... 请注意这里两个参数类型的依赖关系,任何要从源链表 src 复制过来的对象都必须是对目标链表 dst 元素可赋值的;所以我们可以不管 src 的元素类型是什么,只要它是 T 类型的子类型。
copy 方法的方法头表示了使用一个类型参数,但是用通配符来作为第二个参数的元素类型的依赖关系。
我们是可以用另外一种不用通配符来写这个方法头的办法。
class Collections public static ltT S extends Tgt vod copyListltTgt dest ListltSgt src ... 没问题,但是当第一个类型参数用作 dst 的类型和批二个类型参数 S 的上界的时候,S 它本身在 src 类型里只能使用一次,没有其他的东西依赖于它。
这就意味着我们可以用一个通配符来代替 S 了。
使用通配符比声明显式的类型参数要来得清晰和简单,因此在可能的话都优先使用通配符。
当通配符用于方法头外部,作为成员变量、局部变量和数组的类型的时候,同样也有优势。
请看下面的例子。
看回我们之前画图的那个问题,现在我们想要保留一份画图请求的历史记录。
在我们可以这样来维护这份历史记录, Shape 类里用一个静态的变量表示历史记录,然后在 drawAll方法里面把传递的实参储存到那历史记录变量里头。
static ListltListlt extends Shapegtgt history new ArrayListltListlt extends Shapegtgtpublic void drawAllListlt extends Shapegt shapes history.addLastshapes for Shape s: shapes s.drawthis 最后,我们再次留意一下使用类型参数的命名惯例。
当没有更精确的类型来区分的时候,我们用 T 来表示类型,这是通常是在泛型方法里面的情况。
如果有多个类型参数,我们可以用在字母表中与 T 相邻的字母来表示,比如 S。
如果一个泛型方法出现在一个泛型类里面,一个好的方法就是,应该避免对方法和类使用相同的类型参数以免发生混淆。
这在嵌套泛型类里也一样。
六、与遗留代码的交互 到现在为止,我们所有的例子都是在一个假想的理想世界里面的,就是所有的人都在使用
Java 语言支持泛型的最新版本。
唉,不过在现实中情况却不是那样。
千百万行的代码都是用早期版本的语言来编写的,不可能把它们全部在一夜之间就转换过来。
在后面的第 10 部分,我们将会解决把遗留代码转为用泛型这个问题。
在这部分我们要看的是比较简单的问题:遗留代码与泛型代码如何交互?这个问题分为两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。
六-1 在泛型代码中使用遗留代码当你在享受在代码中使用泛型带来的好处的时候,你怎么样使用遗留代码呢?假设这样一个例子,你要使用 com.Foodlibar.widgets 这个包。
Fooblibar.com 的人要销售一个库存控制系统,主要部分如下:package com.Fooblibar.widgetspublic interface Part ... public class Inventory / Adds a new Assembly to the inventory databse. The assembly is given the name name and consists of a set parts specified by parts. All elements of the collection parts must support the Part interface. / public static void addAssemblyString name Collection parts ... public static Assembly getAssemblyString name ...public interface Assembly Collection getParts//Returns a collection of Parts 现在,你可以用上面的 API 来增加新的代码,它可以很好的保证你调用参数恰当的 addAssembly方法,就是说传递的集合是一个 Part 类型的 Collection对象,当然,泛型是最适合做这个:package com.mycompany.inventoryimport com.Fooblibar.widgets.public class Blade implements Part ...public class Guillotine implements Part public class Main public static void mainSring args CollectionltPartgt c new ArrayListltPartgt c.addnew Guillotine c.addnew Blade Inventory.addAssemblyquotthingeequot c CollectionltPartgt k Inventory.getAssemblyquotthingeequot.getParts 当我们调用 addAssembly 方法的时候,它想要的第二个参数是 Collection类型的,实参是 CollectionltPartgt类型,但却可以,为什么呢?毕竟,大多数集合存储的都不是 Part 对象,所以总的来说,编译器不会知道 Collection 存储的是什么类型的集合。
在正规的泛型代码里面,Collection 都带有类型参数。
当一个像 Collection这样的泛型不带类型参数使用的时候,称之为原生类型。
很多人的第一直觉是 Collection 就是指 CollectionltObjectgt,但从我们先前所看到的可以知道,当需要的对象是 CollectionltObjectgt,而传递的却是CollectionltPartgt对象的时候,是类型不安全的。
确切点的说法是 Collection类型表示一个未知类型的集合,就像 Collectionltgt。
稍等一下,那样做也是不正确的!考虑一下调用 getParts方法,它返回一个 Collection 对象,然后赋值给 k,而 k 是 CollectionltPartgt类型的;如果调用的结果是返回一个 Collectionltgt的对象,这个赋值可能是错误的。
事.
上一篇:
Linux操作系统源代码详细分析
下一篇:
bc80e7a0-d1f2-4595-b21d-01a76798e87a