- 浏览: 336911 次
- 性别:
- 来自: 北京
最新评论
-
piao_bo_yi:
linliangyi2007 写道yangit11 写道to ...
什么是0型文法,1型文法,2型文法,3型文法? -
上帝卖糕的:
你才知道啊~~
什么才是主要的! -
avanry:
1,阅览论文及散文书籍和文章,多写日记,多写论文及散文
论文 ...
怎样提高自己的语言组织能力 -
jiyanliang:
去当半学期老师就OK了,进步绝对
怎样提高自己的语言组织能力 -
linliangyi2007:
yangit11 写道to 楼上
不是没人做,是因为已经有了, ...
什么是0型文法,1型文法,2型文法,3型文法?
泛型指南
1. 介绍
JDK1.5中引入了对java语言的多种扩展,泛型(generics)即其中之一。
这个教程的目标是向您介绍java的泛型(generic)。你可能熟悉其他语言的泛型,最著名的是C++的模板(templates)。如果这样,你很快就会看到两者的相似之处和重要差异。如果你不熟悉相似的语法结构,那么更好,你可以从头开始而不需要忘记误解。
Generics允许对类型进行抽象(abstract over types)。最常见的例子是集合类型(Container types),Collection的类树中任意一个即是。
下面是那种典型用法:
第3行的类型转换有些烦人。通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。但是,这个类型转换是必须的(essential)。编译器只能保证iterator返回的是Object类型。为了保证对Integer类型变量赋值的类型安全,必须进行类型转换。
当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(run time error),因为程序员可能会犯错。
程序员如何才能明确表示他们的意图,把一个list中的内容限制为一个特定的数据类型呢?这是generics背后的核心思想。这是上面程序片断的一个泛型版本:
注意变量myIntList的类型声明。它指定这不是一个任意的List,而是一个Integer的List,写作:List<Integer>。我们说List是一个带一个类型参数的泛型接口(a generic interface that takes a type parameter),本例中,类型参数是Integer。我们在创建这个List对象的时候也指定了一个类型参数。
另一个需要注意的是第3行没了类型转换。
现在,你可能认为我们已经成功地去掉了程序里的混乱。我们用第1行的类型参数取代了第3行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说myIntList被声明为List<Integer>类型,这告诉我们无论何时何地使用myIntList变量,编译器保证其中的元素的正确的类型。与之相反,一个类型转换说明程序员认为在那个代码点上它应该是那种类型。
实际结果是,这可以增加可读性和稳定性(robustness),尤其在大型的程序中。
2.定义简单的泛型
下面是从java.util包中的List接口和Iterator接口的定义中摘录的片断:
这些都应该是很熟悉的,除了尖括号中的部分,那是接口List和Iterator中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces List and Iterator)。
类型参数在整个类的声明中可用,几乎是所有可是使用其他普通类型的地方(但是有些重要的限制,请参考第7部分)。
在介绍那一节我们看到了对泛型类型声明List(the generic type declaration List)的调用,如List<Integer>。在这个调用中(通常称作一个参数化类型a parameterized type),所有出现形式类型参数(formal type parameter,这里是E)都被替换成实体类型参数(actual type argument)(这里是Integer)。
你可能想象,List<Integer>代表一个E被全部替换成Integer的版本:
这种直觉可能有帮助,但是也可能导致误解。
它有帮助,因为List<Integer>的声明确实有类似这种替换的方法。
它可能导致误解,因为泛型声明绝不会实际的被这样替换。没有代码的多个拷贝,源码中没有、二进制代码中也没有;磁盘中没有,内存中也没有。如果你是一个C++程序员,你会理解这是和C++模板的很大的区别。
一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。
类型参数就跟在方法或构造函数中普通的参数一样。就像一个方法有形式参数(formal value parameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formal type parameters)。当一个方法被调用,实参(actual arguments)替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。
一个命名的习惯:我们推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。许多容器类型使用E作为其中元素的类型,就像上面举的例子。在后面的例子中还会有一些其他的命名习惯。
3.泛型和子类继承
让我们测试一下我们对泛型的理解。下面的代码片断合法么?
第1行当然合法,但是这个问题的狡猾之处在于第2行。
这产生一个问题:
一个String的List是一个Object的List么?大多数人的直觉是回答:“当然!”。
好,在看下面的几行:
这里,我们使用lo指向ls。我们通过lo来访问ls,一个String的list。我们可以插入任意对象进去。结果是ls中保存的不再是String。当我们试图从中取出元素的时候,会得到意外的结果。
java编译器当然会阻止这种情况的发生。第2行会导致一个编译错误。
总之,如果Foo是Bar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G<Foo>是G<Bar>的子类型并不成立!!
这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。
这种直觉的问题在于它假定这个集合不改变。我们的直觉认为这些东西都不可改变。
举例来说,如果一个交通部(DMV)提供一个驾驶员里表给人口普查局,这似乎很合理。我们想,一个List<Driver>是一个List<Person>,假定Driver是Person的子类型。实际上,我们传递的是一个驾驶员注册的拷贝。然而,人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。
为了处理这种情况,考虑一些更灵活的泛型类型很有用。到现在为止我们看到的规则限制比较大。
4.通配符(Wildcards)
考虑写一个例程来打印一个集合(Collection)中的所有元素。下面是在老的语言中你可能写的代码:
下面是一个使用泛型的幼稚的尝试(使用了新的循环语法):
问题是新版本的用处比老版本小多了。老版本的代码可以使用任何类型的collection作为参数,而新版本则只能使用Collection<Object>,我们刚才阐述了,它不是所有类型的collections的父类。
那么什么是各种collections的父类呢?它写作: Collection<?>(发音为:"collection of unknown"),就是,一个集合,它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:
现在,我们可以使用任何类型的collection来调用它。注意,我们仍然可以读取c中的元素,其类型是Object。这永远是安全的,因为不管collection的真实类型是什么,它包含的都是objects。但是将任意元素加入到其中不是类型安全的:
因为我们不知道c的元素类型,我们不能向其中添加对象。
add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object,因此把get的返回值赋值给一个Object类型的对象或者放在任何希望是Object类型的地方是安全的。
4.1. 有限制的通配符(Bounded Wildcards)
考虑一个简单的画图程序,它可以用来画各种形状,比如矩形和圆形。
为了在程序中表示这些形状,你可以定义下面的类继承结构:
所有的图形通常都有很多个形状。假定它们用一个list来表示,Canvas里有一个方法来画出所有的形状会比较方便:
现在,类型规则导致drawAll()只能使用Shape的list来调用。它不能,比如说对List<Circle>来调用。这很不幸,因为这个方法所作的只是从这个list读取shape,因此它应该也能对List<Circle>调用。我们真正要的是这个方法能够接受一个任意种类的shape:
public void drawAll(List<? extends Shape> shapes) { //..}
这里有一处很小但是很重要的不同:我们把类型 List<Shape> 替换成了 List<? extends Shape>。现在drawAll()可以接受任何Shape的子类的List,所以我们可以对List<Circle>进行调用。
List<? extends Shape>是有限制通配符的一个例子。这里?代表一个未知的类型,就像我们前面看到的通配符一样。但是,在这里,我们知道这个未知的类型实际上是Shape的一个子类(它可以是Shape本身或者Shape的子类而不必是extends自Shape)。我们说Shape是这个通配符的上限(upper bound)。
像平常一样,要得到使用通配符的灵活性有些代价。这个代价是,现在像shapes中写入是非法的。比如下面的代码是不允许的:
你应该能够指出为什么上面的代码是不允许的。因为shapes.add的第二个参数类型是? extends Shape ——一个Shape未知的子类。因此我们不知道这个类型是什么,我们不知道它是不是Rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个Rectangle不安全。
有限制的通配符正是我们解决DMV给人口普查局传送名单的例子所需要的。我们的例子假定数据用一个姓名(String)到people(用Person或其子类来表示,比如Driver)。Map<K,V>是一个有两个类型参数的泛型类型的例子,表示map的键key和值value。
再一次,注意形式类型参数的命名习惯——K代表keys,V代表vlaues。
5.泛型方法
考虑写一个方法,它用一个Object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。
下面是第一次尝试:
现在,你应该能够学会避免初学者试图使用Collection<Object>作为集合参数类型的错误了。或许你已经意识到使用 Collection<?>也不能工作。会议一下,你不能把对象放进一个未知类型的集合中去。
解决这个问题的办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型参数的值。它通常推断出能使调用类型正确的最明确的类型参数(原文是:It will generally infer the most specific type argument that will make the call type-correct.)。
现在有一个问题:我们应该什么时候使用泛型方法,又什么时候使用通配符类型呢?
为了理解答案,让我们先看看Collection库中的几个方法。
但是,在 containsAll 和 addAll中,类型参数T 都只使用一次。返回值的类型既不依赖于类型参数(type parameter)也不依赖于方法的其他参数(这里,只有简单的一个参数)。这告诉我们类型参数(type argument)被用作多态(polymorphism),它唯一的效果是允许在不同的调用点,可以使用多种实参类型(actual argument)。如果是这种情况,应该使用通配符。通配符就是被设计用来支持灵活的子类化的,这是我们在这里要强调的。
泛型函数允许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。如果没有这样的依赖关系,不应该使用泛型方法。
一前一后的同时使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():
注意两个参数的类型的依赖关系。任何被从源list从拷贝出来的对象必须能够将其指定为目标list(dest) 的元素的类型——T类型。因此源类型的元素类型可以是T的任意子类型,我们不关心具体的类型。
copy方法的签名使用一个类型参数表示了类型依赖,但是使用了一个通配符作为第二个参数的元素类型。我们也可以用其他方式写这个函数的签名而根本不使用通配符:
这也可以,但是第一个类型参数在dst的类型和第二个参数的类型参数S的上限这两个地方都有使用,而S本身只使用一次,在src的类型中——没有其他的依赖于它。这意味着我们可以用通配符来代替S。使用通配符比声明显式的类型参数更加清晰和准确,所以在可能的情况下使用通配符更好。
通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组。这就有一个例子。
回到我们的画图问题,假定我们想要保持画图请求的历史记录。我们可以把历史记录保存在Shape类的一个静态成员变量里,在drawAll() 被调用的时候把传进来的参数保存进历史记录:
最终,再说一下类型参数的命名习惯。
我们使用T 代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数,我们可能使用字母表中T的临近的字母,比如S。如果一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。对内部类也是同样。
6.与旧代码交互
直到现在,我们的例子中都假定了一个理想的世界,那里所有人使用的都是最新版本的java编程语言,它支持泛型。
唉,现实并非如此。百万行代码都是在早先版本的语言下写作的,他们不可能一晚上就转换过来。
后面,在第10部分,我们会解决把老代码转换为使用泛型的代码的问题。在这里,我们把注意力放在一个更简单的问题:老代码怎么和泛型代码交互?这个问题包括两部分:在泛型中使用老代码和在老代码中使用泛型代码。
6.1. 在泛型代码中使用老代码
怎样才能使用老代码的同时在自己的代码中享受泛型带来的好处?
作为一个例子,假定你像使用包 com.Fooblibar.widgets。Fooblibar.com(完全虚构出来的公司) 的人们出售一种进行库存管理的系统,下面是主要代码:
现在,你想使用上述API写新代码。如果能保证调用addAssembly()时总是使用正确的参数会很棒——就是说,你传进去的确实时一个Part的Collection。当然,泛型可以实现这个目的:
当我们调用addAssembly,它希望第二个参数是Collection类型。而实际参数是Collection<Part> 类型。这可以工作,但是为什么?毕竟,大多数集合不包含Part对象,而且总的来说,编译器无法知道Collection指的是什么类型的集合。
在严格的泛型代码里,Collection应该总是带着类型参数。当一个泛型类型,比如Collection被使用而没有类型参数时,它被称作一个raw type(自然类型??)。
大多数人的第一直觉时Collection实际上意味着 Collection<Object>。但是,像我们前面看到的,当需要Collection<Object>时传递 Collection<Part>是不安全的。类型Collection表示一个未知类型元素的集合,就像Collection<?>,这样说更准确。
但是等一下,那也不正确。考虑getParts()这个调用,它返回一个Collection。然后它被赋值给k,而k是Collection<Part>。如果这个调用的结果是一个Collection<?>,这个赋值应该是一个错误。
事实上,这个赋值是合法的,但是它产生一个未检查警告(unchecked warning)。这个警告是必要的,因为事实是编译器无法保证其正确性。我们没有办法检查getAssembly()中的旧代码来保证返回的确实是一个Collection<Part>。代码里使用的类型是Collection,可以合法的向其中加入任何Object。
那么,这应该是一个错误么?理论上讲,Yes,但是实际上讲,如果泛型代码要调用旧代码,那么这必须被允许。这取决于你,程序员,在这种情况下来满足你自己。这个赋值是合法的因为getAssembly()的调用约定中说它返回一个Part的集合,即使这个类型声明中没有显示出这一点。
因此,自然类型和通配符类型很像,但是他们的类型检查不是同样严格。允许泛型与已经存在的老代码相交互是一个深思熟虑的决定。
从泛型代码中调用老代码具有先天的危险性,一旦你把泛型编程和非泛型编程混合起来,泛型系统所提供的所有安全保证都失效。然而,你还是比你根本不用泛型要好。至少你知道你这一端的代码是稳定的。
在非泛型代码远比泛型代码多的时候,不可避免会出现两者必须混合的情况。
如果你发现你不得不混合旧代码和泛型代码,仔细注意未检查警告(unchecked warnings),仔细考虑你怎样才能证明出现警告的部分代码是正确的。
如果你仍然犯了错,而导致警告的代码确实不是类型安全的,那么会发生什么?让我们看一下这种情形。在这个过程中,我们将了解一些编译器工作的内幕。
6.2. 擦除和翻译(Erasure and Translation)
这里,我们用一个老的普通的list的引用来指向一个String的list。我们插入一个Integer到这个list中,并且试图得到一个String。这是明显的错误。如果我们忽略这个警告并且试图运行以上代码,它将在我们试图使用错误的类型的地方失败。在运行的时候,上面的代码与下面的代码的行为一样:
当我们从list中获取一个元素的时候,并且试图通过转换为String而把它当作一个string,我们得到一个 ClassCastException。完全一样的事情发生在使用泛型的代码上。
这样的原因是,泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的。你可以(基本上就是)把它认为是一个从源码到源码的转换,它把泛型版本的loophole()转换成非泛型版本。
结果是,java虚拟机的类型安全和稳定性决不能冒险,即使在又unchecked warning的情况下。
基本上,擦除去掉了所有的泛型类型信息。所有在尖括号之间的类型信息都被扔掉了,因此,比如说一个List<String>类型被转换为List。所有对类型变量的引用被替换成类型变量的上限(通常是Object)。而且,无论何时如果结果代码类型不正确,会插入一个到合适的类型的转换,就像loophole的最后一行那样。
擦除的全部的细节超出了本文的范围,但是我们给出的简单描述与事实很接近。知道一点这个有好处,特别是如果你要作一些复杂的事,比如把现有API转换成使用泛型的代码(第10部分)或者仅仅是想理解为什么会这样。
6.3. 在老代码中使用泛型代码
现在让我们来考虑相反的情形。假定Fooblibar.com公司的人决定把他们的代码转换为使用泛型来实现,但是他们的一些客户没有转换。现在代码就像下面:
客户端代码是在泛型被引入之前完成的,但是它使用了包com.Fooblibar.widgets和集合库,它们都使用了泛型。客户端代码中的泛型类的声明都是使用了自然类型(raw types)。第1行产生一个unchecked warning,因为一个自然的Collection被传递到一个需要Collection<Part>的地方,而编译器无法保证Collection就是一个Collection<Part>。
你还有另一种选择,你可以使用source 1.4 标志来编译客户端代码,以保证不会产生警告。但是这种情况下你无法使用jdk1.5 中的任何新特性。
7.要点(The Fine Print)
7.1. 一个泛型类被其所有调用共享
下面的代码打印的结果是什么?
或许你会说false,但是那你就错了。它打印出true。因为所有的泛型类型在运行时有同样的类(class),而不管他们的实际类型参数。
事实上,泛型之所以为泛型就是因为它对所有其可能的类型参数,它有同样的行为;同样的类可以被当作许多不同的类型。
作为一个结果,类的静态变量和方法也在所有的实例间共享。这就是为什么在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数申明是不合法的原因。
7.2. 转型和instanceof
泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是没有意义的。
类似的,如下的类型转换
Collection<String> cstr = (Collection<String>) cs;
得到一个unchecked warning,因为运行时环境不会为你作这样的检查。
对类型变量也是一样:
类型参数在运行时并不存在。这意味着它们不会添加任何的时间或者空间上的负担,这很好。不幸的是,这也意味着你不能依靠他们进行类型转换。
7.3 数组Arrays
数组对象的组成类型不能是一个类型变量或者类型参数,除非它是无上限的通配符类型。你可以声明元素类型是一个类型参数或者参数化类型的数组类型,但不是数组对象(译注:得不到对象,只能声明)。
这很烦人,但是确实时这样。为了避免下面的情况,必须有这样的限制:
如果参数化类型可以是数组,那么意味着上面的例子可以没有任何unchecked warnings的通过编译,但是在运行时失败。我们把类型安全(type-safety)作为泛型首要的设计目标。特别的,java语言被设计为保证:如果你的整个程序没有unchecked warnings的使用javac –source1.5通过编译,那么它是类型安全的(原文: if your entire application has been compiled without unchecked warnings using javac -source 1.5, it is type safe)。
然和,你仍然可以使用通配符数组。上面的代码有两种变化。第一种改变放弃使用数组对象和元素类型参数化的数组类型。结果是,我们不得不显式的进行类型转换来从数组中获得一个String。
在下面的变体中,我们避免了产生一个元素类型是参数化的数组对象,但是使用了元素类型参数化的类型。(译注:意思如下面的第一行代码所示,声明一个泛型化的数组,但是new的时候使用的是raw type,原文中是 new ArrayList<?>(10),那是错的,已经修正为new ArrayList(10);)这是合法的,但是产生一个unchecked warning。实际上,这个代码是不安全的,最后产生一个错误。
因为类型变量在运行时并不存在,所以没有办法决定实际类型是什么。
解决这些限制的办法是使用字面的类作为运行时类型标志(原文:use class literals as run time type tokens),见第8部分。
8.Class Literals as Run-time Type Tokens
JDK1.5中一个变化是类 java.lang.Class是泛型化的。这是把泛型作为容器类之外的一个很有意思的例子(using genericity for something other than a container class)。
现在,Class有一个类型参数T, 你很可能会问,T 代表什么?
它代表Class对象代表的类型。比如说,String.class类型代表 Class<String>,Serializable.class代表 Class<Serializable>。着可以被用来提高你的反射代码的类型安全。
特别的,因为 Class的 newInstance() 方法现在返回一个T, 你可以在使用反射创建对象时得到更精确的类型。
比如说,假定你要写一个工具方法来进行一个数据库查询,给定一个SQL语句,并返回一个数据库中符合查询条件的对象集合(collection)。
一个方法时显式的传递一个工厂对象,像下面的代码:
然后调用:
这个解决方案的缺点是它需要下面的二者之一:
l 调用处那冗长的匿名工厂类,或
l 为每个要使用的类型声明一个工厂类并传递其对象给调用的地方
这很不自然。
使用class literal作为工厂对象是非常自然的,它可以被发射使用。没有泛型的代码可能是:
但是这不能给我们返回一个我们要的精确类型的集合。现在Class是泛型的,我们可以写:
来通过一种类型安全的方式得到我们要的集合。
这项技术是一个非常有用的技巧,它已成为一个在处理注释(annotations)的新API中被广泛使用的习惯用法。
9.More fun with *
在这一部分,我们来考虑一些通配符得高级用法。我们已经看到了上限通配符在从一个数据结构中进行读取的几个例子。现在考虑相反的情况,一个只写的数据结构。
接口Sink是这种情况的一个简单例子。
我们可以想象他被如下面的代码一样使用。方法writeAll() 被设计来把集合coll的所有元素flush到sink snk,并且返回最后一个flush的元素。
像上面所写,writeAll() 的调用是非法的,因为没有有效的类型参数可以被推断出来。String 或 Object都不是T的合适的类型,因为Collection的元素和 Sink的元素必须是同样的类型。
我们可以解决这个问题,通过使用通配符来修改writeAll()的方法签名,如下:
这个调用现在是合法的,但是赋值产生错误,因为推断出的返回值类型是 Object因为T 匹配了Sink的类型,Object。
解决方案是使用一种我们还没有见过的有限制的通配符:有下限的通配符。语法 ? super T 表示T的一个未知的父类(或者是T自己)。这跟我们用? extends T 表示T的一个未知的子类是对应的。
使用这个语法,这个调用是合法的,推断出来的T是String,正是我们想要的。
现在让我们看一个更现实的例子。一个 java.util.TreeSet<E> 代表一个有序的元素是E类型的树。创建一个TreeSet的一个方法是传递一个 Comparator 对象给构造函数。这个Comparator将会用来按照需要对TreeSet进行排序。
假定我们要创建一个 TreeSet<String> 并传递一个合适的 Comparator,我们需要传一个能比较String的Comparator。这可以是一个 Comparator<String>,也可以是一个 Comparator<Object>。然而我们不能用Comparator<Object>来调用上面的构造函数。我们可以使用一个有下限的通配符来得到我们需要的灵活性:
这允许任何可用的Comparator被传递进去。
作为使用下限通配符最终的例子,让我们来看看方法 Collections.max(),它返回一个集合中的最大的元素。
现在,为了让max()能工作,传进来的集合中的所有元素必须实现 Comparatable接口。而且,他们必须都能够被彼此比较(all be comparable to each other)。第一个尝试是:
就是说,方法的参数是某一个能和自己进行比较的T的集合。这限制太严格了。
为什么?考虑一个能和任何对象进行比较的类型:
cf 中的每个元素都可以和每个cf中的其他元素进行比较,因为每个这样的元素都是一个Foo,它可以和任意的对象进行比较,也可以和另一个Foo进行比较。
但是,使用上面的方法签名,我们发现这个调用被拒绝。推断出来的类型必须是Foo,但是Foo没有实现接口 Comparable<Foo>。
T 精确的(exactly)和自己能比较是不需要的。所需要的是 T能够和它的父类中的一个进行比较,这导出:(注:Collections.max()的实际方法签名更复杂,我们在第10部分再讨论。)
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
这个推论对大多数想让 Comparable 对任意类型生效的用法中都有效:你总是应该使用 Comparable<? super T>。
总之,如果你有一个只使用类型参数T作为参数的API,它的使用应该利用下限通配符( ? super T )的好处。相反的,如果API只返回T,你应该使用上限通配符( ? extends T )来给你的客户端更大的灵活性。
9.1 通配符匹配(wildcard capture)
现在应该很清晰,如果给定:
实际的set是一个String的set并不起作用,起作用的是传进来的表达式是一个unknown type的set,它不能保证是一个String的set或者任何其他的特定类型。
现在,考虑:
似乎这应该不被允许,但是,研究这个特定的调用,允许它是非常安全的。毕竟,unmodifiableSet 确实对任何种类的Set能工作,不管它的元素类型。
因为这种情况相对出现的次数比较多,有一个特殊的规则在能证明代码是安全的情况下允许这样的代码。()这个规则,称为wildcard capture,允许编译器推断出通配符为unknown type作为一个泛型方法的类型参数。
10. 泛型化老代码
前面,我们讲述了新老代码如何交互。现在,是时候研究更难的泛型化老代码的问题了。
如果你决定把老代码转换成使用泛型的代码,你需要仔细考虑怎么修改你的API。
你必须确定泛型化的API不会过分严格,它必须继续支持原来的API调用契约(original contract of the API)。在考虑几个 java.util.Collection中的例子。泛型代码之前的API像:
一个稚嫩的泛型化尝试:
这当然是类型安全的,但是它不支持这个API的原始契约(original contract)。
containsAll() 方法能对所有进来的任意类型的collection工作。它只有在传进来的collection中真正只包含E的实例才成功,但是:
l 传进来的collection的静态类型可能不同,可能是因为调用者不知道传进来的colleciton的精确类型,或者因为它是一个Collection<S>,S是E的子类型。
l 用一个不同类型的collection来调用containsAll()应该是合法的。这个例程应该能够工作,返回false。
对addAll(),我们应该能够添加任何元素是E的子类型的collection。我们已经在第5部分讲述了怎么正确的处理这种情况。
你还应该保证修订过的API保持与老客户端的二进制兼容。者以为者API的erasure必须与老的未泛型化版本一样。在大多数情况下,这是很自然的结果,但是有些精巧的情形(subtle cases)。我们看看我们已经碰到过的精巧的情形中的一个(one of the subtle cases),方法Collections.max()。就像我们在第9部分看到的,一个似是而非的max()的方法签名是:
这很好,除了擦除(erasure)后的签名是:
这和老版本的max() 的签名不同:
当然可以把max()定义为这个签名,但是这没有成为现实,因为所有调用了Collections.max()的老的二进制class文件依赖于返回Object的签名。
我们可以强迫the erasure不同,通过给形式类型参数T显式的定义一个父类。
这是一个对一个类型参数给定多个界限(multiple bounds)的例子,是用语法 T1 & T2 … & Tn。一个有多个界限的类型的参数是所有界限中列出来的类型的子类。当多个界限被使用的时候,界限中的第一个类型被用作这个类型参数的erasure。
最后,我们应该想到max只从传进来的collection中读取数据,因此它对元素是T的子类的collection可用。这给我们JDK中使用的真正的签名:
实际中出现那么棘手的问题是很罕见的,但是专业库设计师应该准备好非常仔细的考虑转换现存的API。
另一个需要小心的问题是协变式返回值(covariant returns),就是说在子类中获得一个方法的返回值(refining the return type of a method in a subclass)。在老API中你无法使用这个特性带来的好处。
为了知其原因,让我们看一个例子。
假定你的原来的API是下面的形式:
JDK1.5中引入了对java语言的多种扩展,泛型(generics)即其中之一。
这个教程的目标是向您介绍java的泛型(generic)。你可能熟悉其他语言的泛型,最著名的是C++的模板(templates)。如果这样,你很快就会看到两者的相似之处和重要差异。如果你不熟悉相似的语法结构,那么更好,你可以从头开始而不需要忘记误解。
Generics允许对类型进行抽象(abstract over types)。最常见的例子是集合类型(Container types),Collection的类树中任意一个即是。
下面是那种典型用法:
List myIntList = new LinkedList();// 1 myIntList.add(new Integer(0));// 2 Integer x = (Integer) myIntList.iterator().next();// 3
第3行的类型转换有些烦人。通常情况下,程序员知道一个特定的list里边放的是什么类型的数据。但是,这个类型转换是必须的(essential)。编译器只能保证iterator返回的是Object类型。为了保证对Integer类型变量赋值的类型安全,必须进行类型转换。
当然,这个类型转换不仅仅带来了混乱,它还可能产生一个运行时错误(run time error),因为程序员可能会犯错。
程序员如何才能明确表示他们的意图,把一个list中的内容限制为一个特定的数据类型呢?这是generics背后的核心思想。这是上面程序片断的一个泛型版本:
List<Integer> myIntList = new LinkedList<Integer>(); // 1 myIntList.add(new Integer(0)); // 2 Integer x = myIntList.iterator().next(); // 3
注意变量myIntList的类型声明。它指定这不是一个任意的List,而是一个Integer的List,写作:List<Integer>。我们说List是一个带一个类型参数的泛型接口(a generic interface that takes a type parameter),本例中,类型参数是Integer。我们在创建这个List对象的时候也指定了一个类型参数。
另一个需要注意的是第3行没了类型转换。
现在,你可能认为我们已经成功地去掉了程序里的混乱。我们用第1行的类型参数取代了第3行的类型转换。然而,这里还有个很大的不同。编译器现在能够在编译时检查程序的正确性。当我们说myIntList被声明为List<Integer>类型,这告诉我们无论何时何地使用myIntList变量,编译器保证其中的元素的正确的类型。与之相反,一个类型转换说明程序员认为在那个代码点上它应该是那种类型。
实际结果是,这可以增加可读性和稳定性(robustness),尤其在大型的程序中。
2.定义简单的泛型
下面是从java.util包中的List接口和Iterator接口的定义中摘录的片断:
public interface List<E> { void add(E x); Iterator<E> iterator(); } public interface Iterator<E> { E next(); boolean hasNext(); }
这些都应该是很熟悉的,除了尖括号中的部分,那是接口List和Iterator中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces List and Iterator)。
类型参数在整个类的声明中可用,几乎是所有可是使用其他普通类型的地方(但是有些重要的限制,请参考第7部分)。
在介绍那一节我们看到了对泛型类型声明List(the generic type declaration List)的调用,如List<Integer>。在这个调用中(通常称作一个参数化类型a parameterized type),所有出现形式类型参数(formal type parameter,这里是E)都被替换成实体类型参数(actual type argument)(这里是Integer)。
你可能想象,List<Integer>代表一个E被全部替换成Integer的版本:
public interface IntegerList { void add(Integer x) Iterator<Integer> iterator(); }
这种直觉可能有帮助,但是也可能导致误解。
它有帮助,因为List<Integer>的声明确实有类似这种替换的方法。
它可能导致误解,因为泛型声明绝不会实际的被这样替换。没有代码的多个拷贝,源码中没有、二进制代码中也没有;磁盘中没有,内存中也没有。如果你是一个C++程序员,你会理解这是和C++模板的很大的区别。
一个泛型类型的声明只被编译一次,并且得到一个class文件,就像普通的class或者interface的声明一样。
类型参数就跟在方法或构造函数中普通的参数一样。就像一个方法有形式参数(formal value parameters)来描述它操作的参数的种类一样,一个泛型声明也有形式类型参数(formal type parameters)。当一个方法被调用,实参(actual arguments)替换形参,方法体被执行。当一个泛型声明被调用,实际类型参数(actual type arguments)取代形式类型参数。
一个命名的习惯:我们推荐你用简练的名字作为形式类型参数的名字(如果可能,单个字符)。最好避免小写字母,这使它和其他的普通的形式参数很容易被区分开来。许多容器类型使用E作为其中元素的类型,就像上面举的例子。在后面的例子中还会有一些其他的命名习惯。
3.泛型和子类继承
让我们测试一下我们对泛型的理解。下面的代码片断合法么?
List<String> ls = new ArrayList<String>(); //1 List<Object> lo = ls; //2
第1行当然合法,但是这个问题的狡猾之处在于第2行。
这产生一个问题:
一个String的List是一个Object的List么?大多数人的直觉是回答:“当然!”。
好,在看下面的几行:
lo.add(new Object()); // 3 String s = ls.get(0); // 4: 试图把Object赋值给String
这里,我们使用lo指向ls。我们通过lo来访问ls,一个String的list。我们可以插入任意对象进去。结果是ls中保存的不再是String。当我们试图从中取出元素的时候,会得到意外的结果。
java编译器当然会阻止这种情况的发生。第2行会导致一个编译错误。
总之,如果Foo是Bar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G<Foo>是G<Bar>的子类型并不成立!!
这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。
这种直觉的问题在于它假定这个集合不改变。我们的直觉认为这些东西都不可改变。
举例来说,如果一个交通部(DMV)提供一个驾驶员里表给人口普查局,这似乎很合理。我们想,一个List<Driver>是一个List<Person>,假定Driver是Person的子类型。实际上,我们传递的是一个驾驶员注册的拷贝。然而,人口普查局可能往驾驶员list中加入其他人,这破坏了交通部的记录。
为了处理这种情况,考虑一些更灵活的泛型类型很有用。到现在为止我们看到的规则限制比较大。
4.通配符(Wildcards)
考虑写一个例程来打印一个集合(Collection)中的所有元素。下面是在老的语言中你可能写的代码:
void printCollection(Collection c) { Iterator i = c.iterator(); for (int k = 0; k < c.size(); k++) { System.out.println(i.next()); } }
下面是一个使用泛型的幼稚的尝试(使用了新的循环语法):
void printCollection(Collection<Object> c) { for (Object e : c) { System.out.println(e); } }
问题是新版本的用处比老版本小多了。老版本的代码可以使用任何类型的collection作为参数,而新版本则只能使用Collection<Object>,我们刚才阐述了,它不是所有类型的collections的父类。
那么什么是各种collections的父类呢?它写作: Collection<?>(发音为:"collection of unknown"),就是,一个集合,它的元素类型可以匹配任何类型。显然,它被称为通配符。我们可以写:
void printCollection(Collection<?> c) { for (Object e : c) { System.out.println(e); } }
现在,我们可以使用任何类型的collection来调用它。注意,我们仍然可以读取c中的元素,其类型是Object。这永远是安全的,因为不管collection的真实类型是什么,它包含的都是objects。但是将任意元素加入到其中不是类型安全的:
Collection<?> c = new ArrayList<String>(); c.add(new Object()); // 编译时错误
因为我们不知道c的元素类型,我们不能向其中添加对象。
add方法有类型参数E作为集合的元素类型。我们传给add的任何参数都必须是一个未知类型的子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。唯一的例外是null,它是所有类型的成员。
另一方面,我们可以调用get()方法并使用其返回值。返回值是一个未知的类型,但是我们知道,它总是一个Object,因此把get的返回值赋值给一个Object类型的对象或者放在任何希望是Object类型的地方是安全的。
4.1. 有限制的通配符(Bounded Wildcards)
考虑一个简单的画图程序,它可以用来画各种形状,比如矩形和圆形。
为了在程序中表示这些形状,你可以定义下面的类继承结构:
public abstract class Shape { public abstract void draw(Canvas c); } public class Circle extends Shape { private int x, y, radius; public void draw(Canvas c) { // ... } } public class Rectangle extends Shape { private int x, y, width, height; public void draw(Canvas c) { // ... } } 这些类可以在一个画布(Canvas)上被画出来: public class Canvas { public void draw(Shape s) { s.draw(this); } }
所有的图形通常都有很多个形状。假定它们用一个list来表示,Canvas里有一个方法来画出所有的形状会比较方便:
public void drawAll(List<Shape> shapes) { for (Shape s : shapes) { s.draw(this); } }
现在,类型规则导致drawAll()只能使用Shape的list来调用。它不能,比如说对List<Circle>来调用。这很不幸,因为这个方法所作的只是从这个list读取shape,因此它应该也能对List<Circle>调用。我们真正要的是这个方法能够接受一个任意种类的shape:
public void drawAll(List<? extends Shape> shapes) { //..}
这里有一处很小但是很重要的不同:我们把类型 List<Shape> 替换成了 List<? extends Shape>。现在drawAll()可以接受任何Shape的子类的List,所以我们可以对List<Circle>进行调用。
List<? extends Shape>是有限制通配符的一个例子。这里?代表一个未知的类型,就像我们前面看到的通配符一样。但是,在这里,我们知道这个未知的类型实际上是Shape的一个子类(它可以是Shape本身或者Shape的子类而不必是extends自Shape)。我们说Shape是这个通配符的上限(upper bound)。
像平常一样,要得到使用通配符的灵活性有些代价。这个代价是,现在像shapes中写入是非法的。比如下面的代码是不允许的:
public void addRectangle(List<? extends Shape> shapes) { shapes.add(0, new Rectangle()); // compile-time error! }
你应该能够指出为什么上面的代码是不允许的。因为shapes.add的第二个参数类型是? extends Shape ——一个Shape未知的子类。因此我们不知道这个类型是什么,我们不知道它是不是Rectangle的父类;它可能是也可能不是一个父类,所以这里传递一个Rectangle不安全。
有限制的通配符正是我们解决DMV给人口普查局传送名单的例子所需要的。我们的例子假定数据用一个姓名(String)到people(用Person或其子类来表示,比如Driver)。Map<K,V>是一个有两个类型参数的泛型类型的例子,表示map的键key和值value。
再一次,注意形式类型参数的命名习惯——K代表keys,V代表vlaues。
public class Census { public static void addRegistry(Map<String, ? extends Person> registry) { ...} }... Map<String, Driver> allDrivers = ...; Census.addRegistry(allDrivers);
5.泛型方法
考虑写一个方法,它用一个Object的数组和一个collection作为参数,完成把数组中所有object放入collection中的功能。
下面是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) { for (Object o : a) { c.add(o); // 编译期错误 } }
现在,你应该能够学会避免初学者试图使用Collection<Object>作为集合参数类型的错误了。或许你已经意识到使用 Collection<?>也不能工作。会议一下,你不能把对象放进一个未知类型的集合中去。
解决这个问题的办法是使用generic methods。就像类型声明,方法的声明也可以被泛型化——就是说,带有一个或者多个类型参数。
static <T> void fromArrayToCollection(T[] a, Collection<T> c){ for (T o : a) { c.add(o); // correct } }
我们可以使用任意集合来调用这个方法,只要其元素的类型是数组的元素类型的父类。
Object[] oa = new Object[100]; Collection<Object> co = new ArrayList<Object>(); fromArrayToCollection(oa, co);// T 指Object String[] sa = new String[100]; Collection<String> cs = new ArrayList<String>(); fromArrayToCollection(sa, cs);// T inferred to be String fromArrayToCollection(sa, co);// T inferred to be Object Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection<Number> cn = new ArrayList<Number>(); fromArrayToCollection(ia, cn);// T inferred to be Number fromArrayToCollection(fa, cn);// T inferred to be Number fromArrayToCollection(na, cn);// T inferred to be Number fromArrayToCollection(na, co);// T inferred to be Object fromArrayToCollection(na, cs);// compile-time error
注意,我们并没有传送真实类型参数(actual type argument)给一个泛型方法。编译器根据实参为我们推断类型参数的值。它通常推断出能使调用类型正确的最明确的类型参数(原文是:It will generally infer the most specific type argument that will make the call type-correct.)。
现在有一个问题:我们应该什么时候使用泛型方法,又什么时候使用通配符类型呢?
为了理解答案,让我们先看看Collection库中的几个方法。
public interface Collection<E> { boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); } 我们也可以使用泛型方法来代替: public interface Collection<E> { <T> boolean containsAll(Collection<T> c); <T extends E> boolean addAll(Collection<T> c); // hey, type variables can have bounds too! }
但是,在 containsAll 和 addAll中,类型参数T 都只使用一次。返回值的类型既不依赖于类型参数(type parameter)也不依赖于方法的其他参数(这里,只有简单的一个参数)。这告诉我们类型参数(type argument)被用作多态(polymorphism),它唯一的效果是允许在不同的调用点,可以使用多种实参类型(actual argument)。如果是这种情况,应该使用通配符。通配符就是被设计用来支持灵活的子类化的,这是我们在这里要强调的。
泛型函数允许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。如果没有这样的依赖关系,不应该使用泛型方法。
一前一后的同时使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():
class Collections { public static <T> void copy(List<T> dest, List<? extends T> src){...} }
注意两个参数的类型的依赖关系。任何被从源list从拷贝出来的对象必须能够将其指定为目标list(dest) 的元素的类型——T类型。因此源类型的元素类型可以是T的任意子类型,我们不关心具体的类型。
copy方法的签名使用一个类型参数表示了类型依赖,但是使用了一个通配符作为第二个参数的元素类型。我们也可以用其他方式写这个函数的签名而根本不使用通配符:
class Collections { public static <T, S extends T> void copy(List<T> dest, List<S> src){...} }
这也可以,但是第一个类型参数在dst的类型和第二个参数的类型参数S的上限这两个地方都有使用,而S本身只使用一次,在src的类型中——没有其他的依赖于它。这意味着我们可以用通配符来代替S。使用通配符比声明显式的类型参数更加清晰和准确,所以在可能的情况下使用通配符更好。
通配符还有一个优势式他们可以在方法签名之外被使用,比如field的类型,局部变量和数组。这就有一个例子。
回到我们的画图问题,假定我们想要保持画图请求的历史记录。我们可以把历史记录保存在Shape类的一个静态成员变量里,在drawAll() 被调用的时候把传进来的参数保存进历史记录:
static List<List<? extends Shape>> history = new ArrayList<List<? extends Shape>>(); public void drawAll(List<? extends Shape> shapes) { history.addLast(shapes); for (Shape s: shapes) { s.draw(this); } }
最终,再说一下类型参数的命名习惯。
我们使用T 代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数,我们可能使用字母表中T的临近的字母,比如S。如果一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。对内部类也是同样。
6.与旧代码交互
直到现在,我们的例子中都假定了一个理想的世界,那里所有人使用的都是最新版本的java编程语言,它支持泛型。
唉,现实并非如此。百万行代码都是在早先版本的语言下写作的,他们不可能一晚上就转换过来。
后面,在第10部分,我们会解决把老代码转换为使用泛型的代码的问题。在这里,我们把注意力放在一个更简单的问题:老代码怎么和泛型代码交互?这个问题包括两部分:在泛型中使用老代码和在老代码中使用泛型代码。
6.1. 在泛型代码中使用老代码
怎样才能使用老代码的同时在自己的代码中享受泛型带来的好处?
作为一个例子,假定你像使用包 com.Fooblibar.widgets。Fooblibar.com(完全虚构出来的公司) 的人们出售一种进行库存管理的系统,下面是主要代码:
package com.Fooblibar.widgets; public interface Part { ...} public class Inventory { /** * 添加一个新配件到库存数据库 * 配件有名字name, 并由零件(Part)的集合组成。 * 零件由parts 指定. collection parts 中的元素必须实现Part接口。 **/ public static void addAssembly(String name, Collection parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { Collection getParts(); // Returns a collection of Parts }
现在,你想使用上述API写新代码。如果能保证调用addAssembly()时总是使用正确的参数会很棒——就是说,你传进去的确实时一个Part的Collection。当然,泛型可以实现这个目的:
package com.mycompany.inventory; import com.Fooblibar.widgets.*; public class Blade implements Part { ... } public class Guillotine implements Part { } public class Main { public static void main(String[] args) { Collection<Part> c = new ArrayList<Part>(); c.add(new Guillotine()) ; c.add(new Blade()); Inventory.addAssembly(”thingee”, c); Collection<Part> k = Inventory.getAssembly(”thingee”).getParts(); } }
当我们调用addAssembly,它希望第二个参数是Collection类型。而实际参数是Collection<Part> 类型。这可以工作,但是为什么?毕竟,大多数集合不包含Part对象,而且总的来说,编译器无法知道Collection指的是什么类型的集合。
在严格的泛型代码里,Collection应该总是带着类型参数。当一个泛型类型,比如Collection被使用而没有类型参数时,它被称作一个raw type(自然类型??)。
大多数人的第一直觉时Collection实际上意味着 Collection<Object>。但是,像我们前面看到的,当需要Collection<Object>时传递 Collection<Part>是不安全的。类型Collection表示一个未知类型元素的集合,就像Collection<?>,这样说更准确。
但是等一下,那也不正确。考虑getParts()这个调用,它返回一个Collection。然后它被赋值给k,而k是Collection<Part>。如果这个调用的结果是一个Collection<?>,这个赋值应该是一个错误。
事实上,这个赋值是合法的,但是它产生一个未检查警告(unchecked warning)。这个警告是必要的,因为事实是编译器无法保证其正确性。我们没有办法检查getAssembly()中的旧代码来保证返回的确实是一个Collection<Part>。代码里使用的类型是Collection,可以合法的向其中加入任何Object。
那么,这应该是一个错误么?理论上讲,Yes,但是实际上讲,如果泛型代码要调用旧代码,那么这必须被允许。这取决于你,程序员,在这种情况下来满足你自己。这个赋值是合法的因为getAssembly()的调用约定中说它返回一个Part的集合,即使这个类型声明中没有显示出这一点。
因此,自然类型和通配符类型很像,但是他们的类型检查不是同样严格。允许泛型与已经存在的老代码相交互是一个深思熟虑的决定。
从泛型代码中调用老代码具有先天的危险性,一旦你把泛型编程和非泛型编程混合起来,泛型系统所提供的所有安全保证都失效。然而,你还是比你根本不用泛型要好。至少你知道你这一端的代码是稳定的。
在非泛型代码远比泛型代码多的时候,不可避免会出现两者必须混合的情况。
如果你发现你不得不混合旧代码和泛型代码,仔细注意未检查警告(unchecked warnings),仔细考虑你怎样才能证明出现警告的部分代码是正确的。
如果你仍然犯了错,而导致警告的代码确实不是类型安全的,那么会发生什么?让我们看一下这种情形。在这个过程中,我们将了解一些编译器工作的内幕。
6.2. 擦除和翻译(Erasure and Translation)
public String loophole(Integer x) { List<String> ys = new LinkedList<String>(); List xs = ys; xs.add(x); // compile-time unchecked warning return ys.iterator().next(); }
这里,我们用一个老的普通的list的引用来指向一个String的list。我们插入一个Integer到这个list中,并且试图得到一个String。这是明显的错误。如果我们忽略这个警告并且试图运行以上代码,它将在我们试图使用错误的类型的地方失败。在运行的时候,上面的代码与下面的代码的行为一样:
public String loophole(Integer x) { List ys = new LinkedList(); List xs = ys; xs.add(x); return (String) ys.iterator().next(); // run time error }
当我们从list中获取一个元素的时候,并且试图通过转换为String而把它当作一个string,我们得到一个 ClassCastException。完全一样的事情发生在使用泛型的代码上。
这样的原因是,泛型是通过java编译器的称为擦除(erasure)的前端处理来实现的。你可以(基本上就是)把它认为是一个从源码到源码的转换,它把泛型版本的loophole()转换成非泛型版本。
结果是,java虚拟机的类型安全和稳定性决不能冒险,即使在又unchecked warning的情况下。
基本上,擦除去掉了所有的泛型类型信息。所有在尖括号之间的类型信息都被扔掉了,因此,比如说一个List<String>类型被转换为List。所有对类型变量的引用被替换成类型变量的上限(通常是Object)。而且,无论何时如果结果代码类型不正确,会插入一个到合适的类型的转换,就像loophole的最后一行那样。
擦除的全部的细节超出了本文的范围,但是我们给出的简单描述与事实很接近。知道一点这个有好处,特别是如果你要作一些复杂的事,比如把现有API转换成使用泛型的代码(第10部分)或者仅仅是想理解为什么会这样。
6.3. 在老代码中使用泛型代码
现在让我们来考虑相反的情形。假定Fooblibar.com公司的人决定把他们的代码转换为使用泛型来实现,但是他们的一些客户没有转换。现在代码就像下面:
package com.Fooblibar.widgets; public interface Part { ...} public class Inventory { /** * Adds a new Assembly to the inventory database. * 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 addAssembly(String name, Collection<Part> parts) {...} public static Assembly getAssembly(String name) {...} } public interface Assembly { Collection<Part> getParts(); // Returns a collection of Parts } 客户端代码如下: package com.mycompany.inventory; import com.Fooblibar.widgets.*; public class Blade implements Part { ... } public class Guillotine implements Part { } public class Main { public static void main(String[] args) { Collection c = new ArrayList(); c.add(new Guillotine()) ; c.add(new Blade()); Inventory.addAssembly(”thingee”, c); // 1: unchecked warning Collection k = Inventory.getAssembly(”thingee”).getParts(); } }
客户端代码是在泛型被引入之前完成的,但是它使用了包com.Fooblibar.widgets和集合库,它们都使用了泛型。客户端代码中的泛型类的声明都是使用了自然类型(raw types)。第1行产生一个unchecked warning,因为一个自然的Collection被传递到一个需要Collection<Part>的地方,而编译器无法保证Collection就是一个Collection<Part>。
你还有另一种选择,你可以使用source 1.4 标志来编译客户端代码,以保证不会产生警告。但是这种情况下你无法使用jdk1.5 中的任何新特性。
7.要点(The Fine Print)
7.1. 一个泛型类被其所有调用共享
下面的代码打印的结果是什么?
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
或许你会说false,但是那你就错了。它打印出true。因为所有的泛型类型在运行时有同样的类(class),而不管他们的实际类型参数。
事实上,泛型之所以为泛型就是因为它对所有其可能的类型参数,它有同样的行为;同样的类可以被当作许多不同的类型。
作为一个结果,类的静态变量和方法也在所有的实例间共享。这就是为什么在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数申明是不合法的原因。
7.2. 转型和instanceof
泛型类被所有其实例(instances)共享的另一个暗示是检查一个实例是不是一个特定类型的泛型类是没有意义的。
Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>) { ...} // 非法
类似的,如下的类型转换
Collection<String> cstr = (Collection<String>) cs;
得到一个unchecked warning,因为运行时环境不会为你作这样的检查。
对类型变量也是一样:
<T> T badCast(T t, Object o) { return (T) o; // unchecked warning }
类型参数在运行时并不存在。这意味着它们不会添加任何的时间或者空间上的负担,这很好。不幸的是,这也意味着你不能依靠他们进行类型转换。
7.3 数组Arrays
数组对象的组成类型不能是一个类型变量或者类型参数,除非它是无上限的通配符类型。你可以声明元素类型是一个类型参数或者参数化类型的数组类型,但不是数组对象(译注:得不到对象,只能声明)。
这很烦人,但是确实时这样。为了避免下面的情况,必须有这样的限制:
List<String>[] lsa = new List<String>[10]; // not really allowed Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // unsound, but passes run time store check String s = lsa[1].get(0); // run-time error - ClassCastException
如果参数化类型可以是数组,那么意味着上面的例子可以没有任何unchecked warnings的通过编译,但是在运行时失败。我们把类型安全(type-safety)作为泛型首要的设计目标。特别的,java语言被设计为保证:如果你的整个程序没有unchecked warnings的使用javac –source1.5通过编译,那么它是类型安全的(原文: if your entire application has been compiled without unchecked warnings using javac -source 1.5, it is type safe)。
然和,你仍然可以使用通配符数组。上面的代码有两种变化。第一种改变放弃使用数组对象和元素类型参数化的数组类型。结果是,我们不得不显式的进行类型转换来从数组中获得一个String。
List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // correct String s = (String) lsa[1].get(0); // run time error, but cast is explicit
在下面的变体中,我们避免了产生一个元素类型是参数化的数组对象,但是使用了元素类型参数化的类型。(译注:意思如下面的第一行代码所示,声明一个泛型化的数组,但是new的时候使用的是raw type,原文中是 new ArrayList<?>(10),那是错的,已经修正为new ArrayList(10);)这是合法的,但是产生一个unchecked warning。实际上,这个代码是不安全的,最后产生一个错误。
List<String>[] lsa = new ArrayList[10]; // unchecked warning - this is unsafe! Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // correct String s = lsa[1].get(0); // run time error, but we were warned 类似的,创建一个元素类型是一个类型变量的数组对象导致一个编译时错误: <T> T[] makeArray(T t) { return new T[100]; // error }
因为类型变量在运行时并不存在,所以没有办法决定实际类型是什么。
解决这些限制的办法是使用字面的类作为运行时类型标志(原文:use class literals as run time type tokens),见第8部分。
8.Class Literals as Run-time Type Tokens
JDK1.5中一个变化是类 java.lang.Class是泛型化的。这是把泛型作为容器类之外的一个很有意思的例子(using genericity for something other than a container class)。
现在,Class有一个类型参数T, 你很可能会问,T 代表什么?
它代表Class对象代表的类型。比如说,String.class类型代表 Class<String>,Serializable.class代表 Class<Serializable>。着可以被用来提高你的反射代码的类型安全。
特别的,因为 Class的 newInstance() 方法现在返回一个T, 你可以在使用反射创建对象时得到更精确的类型。
比如说,假定你要写一个工具方法来进行一个数据库查询,给定一个SQL语句,并返回一个数据库中符合查询条件的对象集合(collection)。
一个方法时显式的传递一个工厂对象,像下面的代码:
interface Factory<T> { public T[] make(); } public <T> Collection<T> select(Factory<T> factory, String statement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for (int i=0;i<10;i++/* iterate over jdbc results */ ) { T item = factory.make(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; } 你可以这样调用: select(new Factory<EmpInfo>(){ public EmpInfo make() { return new EmpInfo(); } } , ”selection string”); 也可以声明一个类 EmpInfoFactory 来支持接口 Factory: class EmpInfoFactory implements Factory<EmpInfo> { ... public EmpInfo make() { return new EmpInfo();} }
然后调用:
select(getMyEmpInfoFactory(), "selection string");
这个解决方案的缺点是它需要下面的二者之一:
l 调用处那冗长的匿名工厂类,或
l 为每个要使用的类型声明一个工厂类并传递其对象给调用的地方
这很不自然。
使用class literal作为工厂对象是非常自然的,它可以被发射使用。没有泛型的代码可能是:
Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”); ... public static Collection select(Class c, String sqlStatement) { Collection result = new ArrayList(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { Object item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
但是这不能给我们返回一个我们要的精确类型的集合。现在Class是泛型的,我们可以写:
Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”); ... public static <T> Collection<T> select(Class<T>c, String sqlStatement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { T item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
来通过一种类型安全的方式得到我们要的集合。
这项技术是一个非常有用的技巧,它已成为一个在处理注释(annotations)的新API中被广泛使用的习惯用法。
9.More fun with *
在这一部分,我们来考虑一些通配符得高级用法。我们已经看到了上限通配符在从一个数据结构中进行读取的几个例子。现在考虑相反的情况,一个只写的数据结构。
接口Sink是这种情况的一个简单例子。
interface Sink<T> { void flush(T t); }
我们可以想象他被如下面的代码一样使用。方法writeAll() 被设计来把集合coll的所有元素flush到sink snk,并且返回最后一个flush的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk) { T last = null; for (T t : coll) { last = t; snk.flush(last); } return last; } Sink<Object> s; Collection<String> cs; String str = writeAll(cs, s); // 非法的调用!!
像上面所写,writeAll() 的调用是非法的,因为没有有效的类型参数可以被推断出来。String 或 Object都不是T的合适的类型,因为Collection的元素和 Sink的元素必须是同样的类型。
我们可以解决这个问题,通过使用通配符来修改writeAll()的方法签名,如下:
<T> T writeAll(Collection<? extends T> coll, Sink<T> snk) { … } String str = writeAll(cs, s); //可以调用但是返回值类型错误
这个调用现在是合法的,但是赋值产生错误,因为推断出的返回值类型是 Object因为T 匹配了Sink的类型,Object。
解决方案是使用一种我们还没有见过的有限制的通配符:有下限的通配符。语法 ? super T 表示T的一个未知的父类(或者是T自己)。这跟我们用? extends T 表示T的一个未知的子类是对应的。
<T> T writeAll(Collection<T> coll, Sink<? super T> snk) { … } String str = writeAll(cs, s); // YES!!!
使用这个语法,这个调用是合法的,推断出来的T是String,正是我们想要的。
现在让我们看一个更现实的例子。一个 java.util.TreeSet<E> 代表一个有序的元素是E类型的树。创建一个TreeSet的一个方法是传递一个 Comparator 对象给构造函数。这个Comparator将会用来按照需要对TreeSet进行排序。
TreeSet(Comparator<E> c) Comparator 接口是核心: interface Comparator<T> { int compare(T fst, T snd); }
假定我们要创建一个 TreeSet<String> 并传递一个合适的 Comparator,我们需要传一个能比较String的Comparator。这可以是一个 Comparator<String>,也可以是一个 Comparator<Object>。然而我们不能用Comparator<Object>来调用上面的构造函数。我们可以使用一个有下限的通配符来得到我们需要的灵活性:
TreeSet(Comparator<? super E> c)
这允许任何可用的Comparator被传递进去。
作为使用下限通配符最终的例子,让我们来看看方法 Collections.max(),它返回一个集合中的最大的元素。
现在,为了让max()能工作,传进来的集合中的所有元素必须实现 Comparatable接口。而且,他们必须都能够被彼此比较(all be comparable to each other)。第一个尝试是:
public static <T extends Comparable<T>> T max(Collection<T> coll)
就是说,方法的参数是某一个能和自己进行比较的T的集合。这限制太严格了。
为什么?考虑一个能和任何对象进行比较的类型:
class Foo implements Comparable<Object> {...} ... Collection<Foo> cf = ...; Collections.max(cf); // 应该能工作
cf 中的每个元素都可以和每个cf中的其他元素进行比较,因为每个这样的元素都是一个Foo,它可以和任意的对象进行比较,也可以和另一个Foo进行比较。
但是,使用上面的方法签名,我们发现这个调用被拒绝。推断出来的类型必须是Foo,但是Foo没有实现接口 Comparable<Foo>。
T 精确的(exactly)和自己能比较是不需要的。所需要的是 T能够和它的父类中的一个进行比较,这导出:(注:Collections.max()的实际方法签名更复杂,我们在第10部分再讨论。)
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
这个推论对大多数想让 Comparable 对任意类型生效的用法中都有效:你总是应该使用 Comparable<? super T>。
总之,如果你有一个只使用类型参数T作为参数的API,它的使用应该利用下限通配符( ? super T )的好处。相反的,如果API只返回T,你应该使用上限通配符( ? extends T )来给你的客户端更大的灵活性。
9.1 通配符匹配(wildcard capture)
现在应该很清晰,如果给定:
Set<?> unknownSet = new HashSet<String>(); ... /** 向 Set s 中添加一个元素*/ public static <T> void addToSet(Set<T> s, T t) {...} 这个调用是非法的: addToSet(unknownSet, "abc"); // 非法
实际的set是一个String的set并不起作用,起作用的是传进来的表达式是一个unknown type的set,它不能保证是一个String的set或者任何其他的特定类型。
现在,考虑:
class Collections { ... <T> public static Set<T> unmodifiableSet(Set<T> set) { ... } }... Set<?> s = Collections.unmodifiableSet(unknownSet); // this works! Why?
似乎这应该不被允许,但是,研究这个特定的调用,允许它是非常安全的。毕竟,unmodifiableSet 确实对任何种类的Set能工作,不管它的元素类型。
因为这种情况相对出现的次数比较多,有一个特殊的规则在能证明代码是安全的情况下允许这样的代码。()这个规则,称为wildcard capture,允许编译器推断出通配符为unknown type作为一个泛型方法的类型参数。
10. 泛型化老代码
前面,我们讲述了新老代码如何交互。现在,是时候研究更难的泛型化老代码的问题了。
如果你决定把老代码转换成使用泛型的代码,你需要仔细考虑怎么修改你的API。
你必须确定泛型化的API不会过分严格,它必须继续支持原来的API调用契约(original contract of the API)。在考虑几个 java.util.Collection中的例子。泛型代码之前的API像:
interface Collection { public boolean containsAll(Collection c); public boolean addAll(Collection c); }
一个稚嫩的泛型化尝试:
interface Collection<E> { public boolean containsAll(Collection<E> c); public boolean addAll(Collection<E> c); }
这当然是类型安全的,但是它不支持这个API的原始契约(original contract)。
containsAll() 方法能对所有进来的任意类型的collection工作。它只有在传进来的collection中真正只包含E的实例才成功,但是:
l 传进来的collection的静态类型可能不同,可能是因为调用者不知道传进来的colleciton的精确类型,或者因为它是一个Collection<S>,S是E的子类型。
l 用一个不同类型的collection来调用containsAll()应该是合法的。这个例程应该能够工作,返回false。
对addAll(),我们应该能够添加任何元素是E的子类型的collection。我们已经在第5部分讲述了怎么正确的处理这种情况。
你还应该保证修订过的API保持与老客户端的二进制兼容。者以为者API的erasure必须与老的未泛型化版本一样。在大多数情况下,这是很自然的结果,但是有些精巧的情形(subtle cases)。我们看看我们已经碰到过的精巧的情形中的一个(one of the subtle cases),方法Collections.max()。就像我们在第9部分看到的,一个似是而非的max()的方法签名是:
public static <T extends Comparable<? super T>> T max(Collection<T> coll)
这很好,除了擦除(erasure)后的签名是:
public static Comparable max(Collection coll)
这和老版本的max() 的签名不同:
public static Object max(Collection coll)
当然可以把max()定义为这个签名,但是这没有成为现实,因为所有调用了Collections.max()的老的二进制class文件依赖于返回Object的签名。
我们可以强迫the erasure不同,通过给形式类型参数T显式的定义一个父类。
public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)
这是一个对一个类型参数给定多个界限(multiple bounds)的例子,是用语法 T1 & T2 … & Tn。一个有多个界限的类型的参数是所有界限中列出来的类型的子类。当多个界限被使用的时候,界限中的第一个类型被用作这个类型参数的erasure。
最后,我们应该想到max只从传进来的collection中读取数据,因此它对元素是T的子类的collection可用。这给我们JDK中使用的真正的签名:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
实际中出现那么棘手的问题是很罕见的,但是专业库设计师应该准备好非常仔细的考虑转换现存的API。
另一个需要小心的问题是协变式返回值(covariant returns),就是说在子类中获得一个方法的返回值(refining the return type of a method in a subclass)。在老API中你无法使用这个特性带来的好处。
为了知其原因,让我们看一个例子。
假定你的原来的API是下面的形式:
public class Foo { public Foo create(){...} // Factory, should create an instance of whatever class it is declared in } public class Bar extends Foo { public Foo create(){...} // actually creates a Bar }为了使用协变式返回值的好处,你把它改成:
public class Foo { public Foo create(){...} // Factory, should create an instance of whatever class it is declared in } public class Bar extends Foo { public Bar create(){...} // actually creates a Bar } 现在,假定你的一个第三方客户代码: public class Baz extends Bar { public Foo create(){...} // actually creates a Baz }Java虚拟机并不直接支持不同类型返回值的方法重载。这个特性是由编译器来支持的。因此,除非Baz类被重新编译,它不会正确的重载Bar的create()方法,而且,Baz必须被修改,因为Baz的代码被拒绝,它的create的返回值不是Bar中create返回值的子类
相关推荐
### Java泛型指南经典知识点解析 #### 一、引言 Java 1.5 版本引入了一系列重要的语言特性,其中“泛型”是其中一项关键特性。泛型的引入极大地提高了 Java 语言的类型安全性和代码重用性。本文档旨在深入探讨 ...
Java 1.5 泛型指南中文版 本资源为 Java 1.5 泛型指南中文版的详细笔记,涵盖了泛型的基础知识、泛型类、泛型方法、通配符、擦除和翻译、类型安全、类型参数、实际类型参数、擦除、翻译、转型和 instanceof、数组、...
根据给定的信息,我们可以深入探讨Java 1.5中引入的泛型概念及其应用。...以上就是基于给定文件信息对Java 1.5泛型指南的主要知识点的详细介绍。希望这些信息能帮助读者更好地理解和应用Java中的泛型概念。
### Java 1.5 泛型指南中文版知识点解析 #### 1. 引言 在Java 1.5中引入的泛型是一种强大的类型安全机制,它允许开发者编写能够处理不同类型数据的类和方法,同时保持代码的类型安全性。通过使用泛型,开发者可以...
这份"JAVA5.0泛型指南"文档深入浅出地介绍了泛型的各个方面,帮助开发者更好地理解和应用这一强大的特性。 1. 泛型的基本概念:泛型是一种模板机制,它允许在定义类、接口和方法时声明一种或多种类型参数。类型参数...
本指南包含了一份Java 1.5泛型指南的中文版以及原始的英文教程,方便程序员进行对照学习。 **1. 泛型的概念** 泛型是一种在编写代码时指定类型参数的方式,允许开发者在定义类、接口和方法时声明类型约束。这使得...
### Java 1.5 泛型指南:深入理解与应用 #### 一、引言:Java 1.5 泛型的重要性与背景 在Java 1.5版本中,引入了泛型(Generics)这一重要的特性,为类型安全性和代码重用性带来了革命性的改变。在此之前,开发...
### JAVA的泛型指南知识点详解 #### 一、绪言:理解JAVA泛型的重要性与应用场景 在Java 5中引入的泛型是一个重要的语言特性,它允许开发者在编写类或接口时使用类型参数来实现类型的安全性和重用性。通过泛型,...
### Java泛型指南:深入理解与应用 #### 泛型概览 Java泛型是Java 5.0引入的重要特性之一,它允许程序员在编译时检查类型安全,并且所有的强制转换都是自动和隐式的,提高了代码的重用率。在Java中,泛型是一种在...
Java1.5泛型指南中文版(Java1.5 GenericTutorial)
Java泛型是自Java 1.5版本引入的一项重要特性,它允许在编程时指定容器(如List、Set、Map等)所存储数据的类型,从而增强了类型安全性和代码的可读性。泛型的基本概念包括类型参数(Type Parameter)、形式类型参数...