六-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()方法的返回类型的子类型。