-----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
四、1-有界通配符
http://xoj.blogone.net考虑一个简单的画图程序,它可以画长方形和圆等形状。为了表示这些形状,
你可能会定义这样的一个类层次结构:
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);
}
}
任何的描画通常都包括有几种形状,假设它们用一个链表来表示,那么如果在
Canvas里面有一个方法来画出所有的形状的话,那将会很方便:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
但是现在,类型的规则说drawAll()方法只能对确切的Shape类型链表调用,
比如,它不能对List<Circle>类型调用该方法。那真是不幸,因为这个方法所要
做的就是从链表中读取形状对象,从而对List<Circle>类型对象进行调用。我们
真正所想的是要让这个方法能够接受一个任何形状的类型链表:
public void drawAll(List<? extends Shape> shapes) { ... }
这里有一个很小但很重要的不同点:我们把类型List<Shape>替换为List<? extends Shape>。
现在drawAll()方法可以接受任何Shape子类的链表,我们就可以如愿的对List<Circle>
调用进行啦。
List<? extends Shape>是一个有界通配符的例子。? 表示一个未知类型,
就像我们之前所看到的通配符一样。但是,我们知道在这个例子里面这个未知类型
实际是Shape的子类型(注:它可以是Shape本身,或者是它的子类,无须在字面上
表明它是继承Shape类的)。我们说Shape是通配符的“上界”。
如往常一样,使用通配符带来的灵活性得要付出一定的代价;代码就是现在在
方法里面不能对Shape对象插入元素。例如,下面的写法是不允许的:
public void addRectangle(List<? extends Shape> shapes) {
shapes.add(0, new Rectangle()); //编译错误
}
你应该可以指出为什么上面的代码是不允许的。shapes.add()方法的第二个
参数的类型是 ? 继承Shape,也就是一个未知的Shape的子类型。既然我们不知道
类型是什么,那么我们就不知道它是否是Rectangle的父类型了;它可能是也可能
不是一个父类型,因此在那里传递一个Rectangle的对象是不安全的。
有界通配符正是需要用来处理汽车公司给人口调查局提交数据的例子方法。在
我们的例子里面,我们假设数据表示为姓名(用字符串表示)对人(表示为引用类
型,比如Person或它的子类型Driver等)的映射。Map<K, V>是有两个类型参数的
一个泛型的例子,表示键值映射。
请再一次注意规范类型参数的命名惯例:K表示键,V表示值。
public class Census {
public static void
addRegistry(Map<String, ? extends Person> registry){ ... }
}
...
Map<String, Driver> allDrivers = ...;
Census.addRegistry(allDrivers);
-------------------------------------------------------------------------------------------
五、泛型方法
http://xoj.blogone.net考虑写这样一个方法,它接收一个数组和一个集合(collection)作为参数,
并把数组里的所有对象放到集合里面。
先试试这样:
static void fromArrayToCollection(Object[] a, Collection<?> c){
for (Object o : a){
c.add(o);//编译错误
}
}
到现在,你应该学会了避免把Collection<Object>作为集合参数的类型这种初学
者的错误;你可能或可能没看出使用Collection<?>也是不行的,回想一下,你是不能
把对象硬塞进一个未知类型的集合里面的。
解决这类问题的方法是使用泛型方法。就像类型声明一样,方法也可以声明为泛型
的,就是说,用一个或多个类型参数作为参数。
static <T> void fromArrayToCollection(T[]a, Collection<T> c){
for (T o : a){
c.add(o);//正确
}
}
对于集合元素的类型是数组类型的父类型,我们就可以调用这个方法。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T是对象类型
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T是字符串类型(String)
fromArrayToCollection(sa, co);// T对象类型
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是Number类型
fromArrayToCollection(fa, cn);// T是Number类型
fromArrayToCollection(na, cn);// T是Number类型
fromArrayToCollection(na, co);// T是Number类型
fromArrayToCollection(na, cs);// 编译错误
请注意,我们并没有把实际的类型实参传递给泛型方法,因为编译器会根据
实参的类型为我们推断出类型实参。一般地,编译器推断得到可以正确调用的最
接近的(the most specific)实参类型。
现在有一个问题:我应该什么时候使用泛型方法,什么时候使用通配符类型
呢?为了明白这个问题的答案,我们来看看Collection库里的几个方法:
interface Collection<E>{
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
在这里我们也可以用泛型方法:
interface Collection<E>{
public <T> boolean containsAll(Collection<T> c);
public <? extends E>boolean addAll(Collection<T> c);
//哈哈,类型变量也可以有界!
}
但是,类型参数T在containsAll和addAll两个方法里面都只是用了一次。返
回类型并不依赖于类型参数或其他传递给该方法的实参(这种是只有一个实参的简单
情况)。这就告诉我们类型实参是用于多态的,它的作用只是对不同的调用可以有一
系列的实际的实参类型。如果是那样的话,就应该使用通配符,通配符就是设计来支
持灵活的子类型的,这也是我们这里所要表述的东西。
泛型方法允许类型参数用于表述一个或多个的实参类型对方法或及其返回类型的
依赖关系。如果没有那样的一个依赖关系的话,泛型方法就不应用使用。
也有可能是一前一后一起使用泛型方法和通配符的情况,下面是Collections.copy()
方法:
class Collections {
public static <T> void copy(List<T> dest, list< ? extends T> src) {...}
}
请注意这里两个参数类型的依赖关系,任何要从源链表src复制过来的对象都必
须是对目标链表dst元素可赋值的;所以我们可以不管src的元素类型是什么,只要
它是T类型的子类型。copy方法的方法头表示了使用一个类型参数,但是用通配符来
作为第二个参数的元素类型的依赖关系。
我们是可以用另外一种不用通配符来写这个方法头的办法。
class Collections {
public static <T, S extends T>
vod copy(List<T> dest, List<S> src) { ...}
}
没问题,但是当第一个类型参数用作dst的类型和批二个类型参数S的上界的
时候,S它本身在src类型里只能使用一次,没有其他的东西依赖于它。这就意味
着我们可以用一个通配符来代替S了。使用通配符比声明显式的类型参数要来得清
晰和简单,因此在可能的话都优先使用通配符。
当通配符用于方法头外部,作为成员变量、局部变量和数组的类型的时候,同
样也有优势。请看下面的例子。
看回我们之前画图的那个问题,现在我们想要保留一份画图请求的历史记录。
我们可以这样来维护这份历史记录,在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。如果一个泛
型方法出现在一个泛型类里面,一个好的方法就是,应该避免对方法和类使用相
同的类型参数以免发生混淆。这在嵌套泛型类里也一样。
-----------------------------------------------------------------------------------------------
六、与遗留代码的交互
到现在为止,我们所有的例子都是在一个假想的理想世界里面的,就是所有的
人都在使用Java语言支持泛型的最新版本。
唉,不过在现实中情况却不是那样。千百万行的代码都是用早期版本的语言
来编写的,不可能把它们全部在一夜之间就转换过来。
在后面的第10部分,我们将会解决把遗留代码转为用泛型这个问题。在这部分
我们要看的是比较简单的问题:遗留代码与泛型代码如何交互?这个问题分为两个
部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。
------------------------------------------------------------------------------------------------
六-1 在泛型代码中使用遗留代码
[url=http://xoj.blogone.net][url]
当你在享受在代码中使用泛型带来的好处的时候,你怎么样使用遗留代码呢?
假设这样一个例子,你要使用com.Foodlibar.widgets这个包。Fooblibar.com
的人要销售一个库存控制系统,主要部分如下:
package com.Fooblibar.widgets;
public 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 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(Sring[] 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这样
的泛型不带类型参数使用的时候,称之为原生类型。
很多人的第一直觉是Collection就是指Collection<Object>,但从我们先前所
看到的可以知道,当需要的对象是Collection<Object>,而传递的却是Collection<Part>
对象的时候,是类型不安全的。确切点的说法是Collection类型表示一个未知类型的
集合,就像Collection<?>。
稍等一下,那样做也是不正确的!考虑一下调用getParts()方法,它返回一个
Collection对象,然后赋值给k,而k是Collection<Part>类型的;如果调用的结果
是返回一个Collection<?>的对象,这个赋值可能是错误的。
事实上,这个赋值是允许的,只是它会产生一个未检测警告。警告是需要的,因为
编译器不能保证赋值的正确性。我们没有办法通过检测遗留代码中的getAssembly()方法
来保证返回的集合的确是一个类型参数是Part的集合。程序里面的类型是Collection,
我们可以合法的对此集合插入任何对象。
所以,这不应该是错误的吗?理论上来说,答案是:是;但实际上如果是泛型代码
调用遗留代码的话,这又是允许的。对这个赋值是否可接受,得取决于程序员自己,在
这个例子中赋值是安全的,因为getAssembly()方法约定是返回以Part作为类型参数的
集合,尽管在类型标记中没有表明。
所以原生类型很像通配符类型,但它们没有那么严格的类型检测。这是有意设计成
这样的,从而可以允许泛型代码可以与之前已有的遗留代码交互。
在泛型代码中调用遗留代码固然是危险的,一旦把泛型代码和非泛型代码混合在一
起,泛型系统所提供的全部安全保证就都变得无效了。但这仍比根本不使用泛型要好,
最起码你知道你的代码是一致的。
泛型代码出现的今天,仍然有很多非泛型代码,二者混合同时使用是不可避免的。
如果一定要把遗留代码与泛型代码混合使用,请小心留意那些未检测警告。仔细的
想想如何才能判定引发警告的代码是安全的。
如果仍然出错,代码引发的警告实际不是类型安全的,那又怎么样呢?我们会看
那样的情况,接下来,我们将会部分的观察编译器的工作方式。
--------------------------------------------------------------------------------------------------
六-2 擦除和翻译
public String loophole(Integer x){
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x);//编译时未检测警告
return ys.iterator().next();
}
在这里我们定义了一个字符串类型的链表和一个一般的老式链表,我们先插入
一个Integer对象,然后试图取出一个String对象,很明显这是错误的。如果我们
忽略警告继续执行代码的话,程序将会在我们使用错误类型的地方出错。在运行时,
代码执行大致如下:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String)ys.iterator().next();//运行时出错
}
当我们要从链表中取出一个元素,并把它当作是一个字符串对象而把它转换为
String类型的时候,我们将会得到一个ClassCastException类型转换异常。在
泛型版本的loophole()方法里面发生的就是这种情况。
出现这种情况的原因是,Java的泛型是通过一个前台转换“擦除”的编译器实现
的,你基本上可以认为它是一个源码对源码的翻译,这就是为何泛型版的loophole()
方法转变为非泛型版本的原因。
结果是,Java虚拟机的类型安全性和完整性永远不会有问题,就算出现未检测
的警告。
基本上,擦除会除去所有的泛型信息。尖括号里面的所有类型信息都会去掉,比
如,参数化类型的List<String>会转换为List。类型变量在之后使用时会被类型
变量的上界(通常是Object)所替换。当最后代码不是类型正确的时候,就会加入
一个适当的类型转换,就像loophole()方法的最后一行。
对“擦除”的完整描述不是本指南的范围内的内容,但前面我们所给的简单描述
也差不多是那样了。了解这点很有好处,特别是当你想做诸如把现有API转为使用
泛型(请看第10部分)这样复杂的东西,或者是想知道为什么它们会那样的时候。
-----------------------------------------------------------------------------------------------
六-3 在遗留代码中使用泛型
现在我们来看看相反的情况。假设Fooblibar.com把他们的API转换为泛型的,
但有些客户还没有转换。代码就会像下面的:
package com.Fooblibar.widgets;
public interface Part { ... }
publlic 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();//Return 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包和集
合库,两个现在都是在用泛型的。在客户代码里面使用的泛型全部都是原生类型。
第1行产生一个未检测警告,因为把一个原生Collection传递给了一个需要Part类型的
Collection的地方,编译器不能保证原生的Collection是一个Part类型的Collection。
不这样做的话,你也可以在编译客户代码的时候使用source 1.4这个标记来保证不
会产生警告。但是这样的话你就不能使用所有JDK 1.5引入的新的语言特性。
-----------------------------------------------------------------------------------------------
七、晦涩难懂的部分
七-1 泛型类为所有调用所共享
下面的代码段会打印出什么呢?
List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会说是false,但是你错了,打印的是true,因为所有泛型类的实例它们
的运行时的类(run-time class)都是一样的,不管它们实际类型参数如何。
泛型类之所以为泛型的,是因为它对所有可能的类型参数都有相同的行为,相同
的类可以看作是有很多不同的类型。
结果就是,一个类的静态的变量和方法也共享于所有的实例中,这就是为什么不
允许在静态方法或初始化部分、或者在静态变量的声明或初始化中引用类型参数。
---------------------------------------------------------------------------------------------
七-2 强制类型转换和instanceof
泛型类在它所有的实例****享,就意味着判断一个实例是否是一个特别调用的泛
型的实例是毫无意义的:
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) {...}//非法
类似地,像这样的强制类型转换:
Collection<String> cstr = (Collection<String>) cs;//未检测警告
给出了一个未检测的警告,因为这里系统在运行时并不会检测。
对于类型变量也一样:
<T> T BadCast(T t, Object o) {
return (T) o;//未检测警告
}
类型变量不存在于运行时,这就是说它们对时间或空间的性能不会造成影响。
但也因此而不能通过强制类型转换可靠地使用它们了。
------------------------------------------------------------------------------------------
七-3 数组
数组对象的组件类型可能不是一个类型变量或一个参数化类型,除非它是一个
(无界的)通配符类型。你可以声明元素类型是类型变量和参数华类型的数组类型,
但元素类型不能是数组对象。
这自然有点郁闷,但这个限制对避免下面的情况是必要的:
List<Strign>[] lsa = new List<String>[10];//实际上是不允许的
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(8));
oa[1] = li;//不合理,但可以通过运行时的赋值检测
String s = lsa[1].get(0);//运行时出错:ClassCastException异常
如果参数化类型的数组允许的话,那么上面的例子编译时就不会有未检测的警告,
但在运行时出错。对于泛型编程,我们的主要设计目标是类型安全,而特别的是这个
语言的设计保证了如果使用了javac -source 1.5来编译整个程序而没有未检测的
警告的话,它是类型安全的。
但是你仍然会使用通配符数组,这与上面的代码相比有两个变化。首先是不使用
数组对象或元素类型被参数化的数组类型,这样我们就需要在从数组中取出一个字符
串的时候进行强制类型转换:
List<?>[] lsa = new List<?>[10];//没问题,无界通配符类型数组
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li;//正确
String s = (String) lsa[1].get(0);//运行时错误,显式强制类型转换
第二个变化是,我们不创建元素类型被参数化的数组对象,但仍然使用参数化元素
类型的数组类型,这是允许的,但引起现未检测警告。这样的程序实际上是不安全的,
甚至最终会出错。
List<String>[] lsa = new List<?>[10];//未检测警告-这是不安全的!
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<integer>();
li.add(new Integer(3));
oa[1]=li;//正确
String s = lsa[1].get(0);//运行出错,但之前已经被警告
类似地,想创建一个元素类型是类型变量的数组对象的话,将会编译出错。
<T> T[] makeArray(T t){
return new T[100];//错误
}
因为类型变量并不存在于运行时,所以没有办法知道实际的数组类型是什么。
要突破这类限制,我们可以用第8部分说到的用类名作为运行时标记的方法。
------------------------------------------------------------------------------------------
八、 把类名作为运行时的类型标记
JDK1.5中的一个变化是java.lang.Class是泛化的,一个有趣的例子是对
容器外的东西使用泛型。
现在Class类有一个类型参数T,你可能会问,T代表什么啊?它就代表Class
对象所表示的类型。
比如,String.class的类型是Class<String>,Serializable.class的
类型是Class<Serializable>,这可以提高你的反射代码中的类型安全性。
特别地,由于现在Class类中的newInstance()方法返回一个T对象,因此
在通过反射创建对象的时候可以得到更精确的类型。
其中一个方法就是显式传入一个factory对象,代码如下:
interface Factory<T> {T make();}
public <T> Collection<T> select(Factory<T> factory, String statement){
Collection<T> result = new ArrayList<T>();
//用JDBC运行SQL查询
for(/*遍历JDBC结果*/){
T item = factory.make();
/*通过SQL结果用反射和设置数据项*/
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");
这种解决办法需要下面的其中之一:
· 在调用的地方使用详细的匿名工厂类(verbose anonymous factory classes),或者
· 为每个使用的类型声明一个工厂类,并把工厂实例传递给调用的地方,这样有点不自然。
使用类名作为一个工厂对象是非常自然的事,这样的话还可以为反射所用。现在
没有泛型的代码可能写作如下:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/*用JDBC执行SQL查询*/
for(/*遍历JDBC产生的结果*/){
Object item = c.newInstance();
/*通过SQL结果用反射和设置数据项*/
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>();
/*用JDBC执行SQL查询*/
for(/*遍历JDBC产生的结果*/){
T item = c.newInstance();
/*通过SQL结果用反射和设置数据项*/
result.add(item);
}
return result;
}
这样就通过类型安全的方法来得到了精确的集合类型了。
这种使用类名作为运行时类型标记的技术是一个很有用的技巧,是需要知道的。
在处理注释的新的API中也有很多类似的情况。
----------------------------------------------------------------------------------------------
九 通配符的其他作用
(more fun with wildcards,不知道如何译才比较妥当,呵呵。)
在这部分,我们将会仔细看看通配符的几个较为深入的用途。我们已经从几个
有界通配符的例子中看到,它对从某一数据结构中读取数据是很有用的。现在来看
看相反的情况,只对数据结构进行写操作。
下面的Sink接口就是这类情况的一个简单的例子:
interface Sink<T> {
flush(T t);
}
我们可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合
里的所有元素填充(flush)到Sink接口变量snk中,并返回最后一个填充的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk){
T last;
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()的方法头来处理,如下:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s);//调用没问题,但返回类型错误
现在调用是合法的了,但由于T的类型跟元素类型是Object的s一样,因为返回的
类型也是Object,因此赋值是不正确的。
解决办法是使用我们之前从未见过的一种有界通配符形式:带下界的通配符。
语法 ? super T 表示了是未知的T的父类型,这与我们之前所使用的有界
(父类型:或者T类型本身,要记住的是,你类型关系是自反的)
通配符是对偶有界通配符,即用 ? extends T 表示未知的T的子类型。
public static<T> T writeAll(Collection<T> coll, Sink<? super T> snk) {...}
...
String str = writeAll(cs, s);//正确!
使用这个语法的调用是合法的,指向的类型是所期望的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
对象,我们传递的Comparator是能够比较字符串的。我们可以用Comparator<String>,
但Comparator<Object>也是可以的。但是,我们不能对Comparator<Object>对象
调用上面所给的构造函数,我们可以用一个下界通配符来得到我们想要的灵活性:
TreeSet(Comparator<? super E> c)
这样就可以使用适合的Comparator对象啦。
最后一个下界通配符的例子,我们来看看Collections.max()方法,这个方法
返回作为参数传递的Collection对象中最大的元素。
现在,为了max()方法能正常运行,传递的Collection对象中的所有元素都必
须是实现了Comparable接口的,还有就是,它们之间必须是可比较的。
先试一下泛化方法头的写法:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
那样,方法就接受一个自身可比较的(comparable)某个T类型的Collection
对象,并返回T类型的一个元素。这样显得太束缚了。
来看看为什么,假设一个类型可以与合意的对象进行比较:
class Foo implements Comparable<Object> {...}
...
Collection<Foo> cf = ...;
Collectins.max(cf);//应该可以正常运行
cf里的每个对象都可以和cf里的任意其他元素进行比较,因为每个元素都是Foo
的对象,而Foo对象可以与任意的对象进行比较,特别是同是Foo对象的。但是,使用
上面的方法头,我们会发现这样的调用是不被接受的,指向的类型必须是Foo,但Foo
并没有实现Comparable<Foo>。
T对于自身的可比性不是必须的,需要的是T与其父类型是可比的,就像下面:
(实际的Collections.max()方法头在后面的第10部分将会讲得更多)
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
这样推理出来的结果基本上适用于想用Comparable来用于任意类型的用法:
就是你想这样用Comparable<? super T>。
总的来说,如果你有一个只能一个T类型参数作为实参的API的话,你就应该用
下界通配符类型(? suer T);相反,如果API只返回T对象,你就应该用上界通
配符类型(? extends T),以使得你的客户的代码有更大的灵活性。
-------------------------------------------------------------------------------------------
九-1 通配符捕捉(?wildcard capture)
现在应该很清楚,给出下面的例子:
Set<?> unknownSet = new HashSet<String>();
...
/** 给Set对象s添加一个元素t*/
public static <T> void addToSet<Set<T> s, T t) {...}
下面的调用是非法的。
addToSet(unknownSet, "abc");//非法的
这无异于实际传递的Set对象是一个字符串类型的Set对象,问题是作为实参传递的
是一个未知类型的Set对象,这样就不能保证它是一个字符串类型或其他类型的Set对象。
现在,来看下面:
class Collections{
...
<T> public static Set<T> unmodifiableSet<Set<T> set) {...}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet);//这是可以的,
//为什么呢?
看起来这应该是不允许的,但是请看看这个特别的调用,这完全是安全的,因此
这是允许的。这里的unmodifiableSet()确实是对任何类型的Set都适合,不管它的
元素类型是什么。
因为这种情况出现得相对频繁,因此就有一个特殊的规则,对代码能够被检验是
安全的任何特定的环境,那样的代码都是允许的。这个规则就是所谓的“通配符捕捉”,
允许编译器对泛型方法引用未知类型的通配符作为类型实参。
-------------------------------------------------------------------------------------------------------
十 把遗留代码转化为泛型代码
早前,我们展示了如何使泛型代码和遗留代码交互,现在该是时候来看看更难的
问题:把老代码改为泛型代码。
如果决定了把老代码转换为泛型代码,你必须慎重考虑如何修改你的API。
你不能对泛型API限制得太死,它得要继续支持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的最初约定,containsAll()方法接受
传入的任何类型的Collection对象,只有当Collection对象中只包括E类型的实例
的时候才正确。但是:
· 传入的Collection对象的静态类型可能不同,这样的原因可能是调用者不知道
传入的Collection对象的精确类型,又或者它是Collection<S>类型的,其中S是E的
子类型。
· 对不同类型的Collection对象调用方法containsAll()完全是合法的,程序
应该能够运行,返回的是false值。
对于addAll()方法这种情况,我们应该能够添加任何存在了E类型的子类型的Collection
对象,我们在第5部分中看过了如何正确处理这种情况。
还要保证改进的API能够保留对老客户的二进制支持(? binary compatibility)。
这就意味着API“擦除”后(erasure)必须与最初的非泛型API一致。在大多数的情
况的结果是自然而然的,但有些小地方却不尽如此。我们将仔细去看看我们之前遇到
过的最小的Collections.max()方法,正如我们在第9部分所见,似乎正确的max()
的方法头:
public static <T extends Comparable<? super T>>
T max(CollectionT> coll)
基本没问题,除了方法头被“擦除”后的情况:
public static Comparable max(Collection coll)
这与max()方法最初的方法头不一样:
public static Object max(Collection coll)
本来是想得到想要的max()方法头,但是没成功,所有老的二进制class文件
调用的Collections.max()都依赖于一个返回Object类型的方法头。
我们可以在类型参数T的边界中显式指定一个父类型来强制改变“擦除”的结果。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
这是一个对类型参数给出多个边界的例子,语法是这样:T1 & T2 ... & Tn.
多边界类型变量对边界类型列表中的所有类型的子类型都是可知的,当使用多边界
类型的时候,边界类型列表中的第一个类型将被作为类型变量“擦除”后的类型。
最后,我们应该记住max()方法只是从传入的Collection方法中读取数据,
因此也就适用于类型是T的子类型的任何Collection对象。
这样就有了我们JDK中实际的方法头:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在实践中很少会有涉及到这么多东西的情况,但专业类型设计者在转换现有的API
的时候应该有所准备的仔细思虑。
另一个问题就是要小心协变返回(covariant returns)的情况,那就是改进
子类中方法的返回类型。你不应该在老API中使用这个特性。
假设你最初的API是这样的:
public class Foo {
public Foo create {...}//工厂方法,应该是创建声明的类的一个实例
}
public class Bar extends Foo {
public Foo create() {...}//实际是创建一个Bar实例
}
用协变返回的话,是这样改:
public class Foo {
public Foo create {...}//工厂方法,应该是创建声明的类的一个实例
}
public class Bar extends Foo {
public Bar create() {...}//实际是创建一个Bar实例
}
现在,假设有这样的第三方客户代码:
public class Baz extends Bar {
public Foo create() {...} //实际是创建一个Baz实例
}
Java虚拟机不直接支持不同返回类型的方法的覆盖,编译器就是支持这样的
特性。结果就是,除非重编译Baz类,否则的话它不会正确覆盖Bar中的create()
方法。此外,Baz类需要修改,因为上面写的代码不能通过编译,Baz中create()
方法的返回类型不是Bar类中create()方法的返回类型的子类型。
十一、鸣谢(这里就不翻译了)
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen,
Peter von der Ah′e and Philip Wadler contributed material to this
tutorial.
Thanks to David Biesack, Bruce Chapman, David Flanagan, Neal Gafter,
¨ Orjan Petersson, Scott Seligman, Yoshiki Shibata and Kresten Krab
Thorup for valuable feedback on earlier versions of this tutorial.
Apologies to anyone whom I’ve forgotten.