5 泛型Ҏ
考虑写一个方法,它包含一个Object数据和一个集合对象,它的作用是将数组中的对象全部插入到集合对象中。下面是W一ơ尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // Compile time error
}
}
到现在ؓ此,你要学会避免新手所犯的错误--试Collection<Object>作ؓq个集合的类型参数。你可能认识或没认识C?Collection<?>也不能完成工作。回忆一下,你不能将对象挤入一个未知类型的集合对象中?br />
处理q些问题的方法是使用泛型Ҏ。就像类型的声明一PҎ的声明也可以泛型?-卻I用一个或多个参数d数化q个Ҏ?br />
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
我们能够调用Lcd的集合对象中的方法,只要q个集合对象中的元素是数l类型中元素的超cd?br />
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co); // T inferred to be 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
注意我们q不需要传递一个确切的cdl泛型方法。编译器会根据准的参数的类型帮我们推断出实际类型参数。编译器通常会推断出大部分的特定cd参数Q这׃得对Ҏ的调用是cd正确的?br />
产生了一个问题:什么时候我应该使用泛型ҎQ什么时候我应用使用通配W类型?Z理解{案Q让我们试一些集合框架类库中的方法:
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我们可能使用下面的泛型方法替换上面的E序Q?br />
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// Hey, type variables can have bounds too!
}
然而,在两个containAll和addAllҎ中,cd参数T只被使用了一ơ。返回类型既不依赖类型参敎ͼ也不需要传递其它的参数l这个方?在本例中Q只不过是一个实参Ş?。这告诉我们该实参用于多态;它的仅有的作用就是允许该Ҏ的多U不同的实参能够应用于不同的调用炏V?br />
泛型Ҏ允许cd参数用于描述一个或多个实参的类型对于该Ҏ?或它的返回g间依赖关pR如果没有这U依赖关p,那么׃应该使用泛型Ҏ?br />
一前一后的使用泛型Ҏ和通配W是可能的,下面的方法Collections.copy()pCq一点: class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
注意q两个参数的cd之间的依赖关pRQ何复制于源表scr的对象对于目标表dest中元素的cdT都必L可赋值的。所以src元素的类型肯定是T的Q何子cd--我们不用兛_q些。复制方法的{使用一个类型参数描qCq种依赖关系Q但通配W用于第二个参数中元素的cd?br />
我们也可以用另一U方法来书写q个Ҏ的签名,q种Ҏ完全不需要用通配W:
class Collections {
public static <T, S extends T>
void copy(List<T> dest, List<S> src) {
...
}
q很好,但当W一个类型参数在cddest和第二个cd的限度中都用了ӞS它那本n只被使用了一ơ,是在src的类型中--没Q何其它的东西再依赖于它了。这是一个我们要以用通配W替换S的一个信受用通配W比昄的声明类型变量更加清晰、更加精,所以在M可能的时候通配W是首选?br />
通配W也有它的优点,它可以被用于Ҏ{的外面,以作为字D늚cdQ局部变量或数组。下面就是这L一个例子?br />
回到我们l制形状的那个例子,假设我们想维护一个绘制Ş状请求的历史记录。我们可以将q个历史记录l护在类Shape内部的一个静态变量,让drawAllҎ它自己获得的实?卌求绘制的形状)加入历史字段中?br />
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表示cdQ只要无需再区别Q何其它的特定cd。这U情늻常用于泛型方法中。如果有多个cd参数Q我可以使字母表中邻qT的其它字母,例如S。如果在一个泛型类中有一个泛型方法,那么Z避免hQ一个好的习惯是不要使泛型类和泛型方法有相同名字的类型参数。这也适用于嵌套泛型类?br />
6 与遗留代码交?/span>
到现在ؓ止,我们的例子是假设处于一U理想的状况Q即每个人都在用JavaE序设计语言的支持泛型的最新版?br />
唉,但现实ƈ非如此。数以百万行计的代码是用Java语言的早期版本写的,而且也不可能在一夜之间就它们{换到新版中?br />
E后Q在"使用泛型转化遗留代码"q一节中Q我们将解决你的旧代码转换C用泛型这个问题。在本节Q我们将x一个简单的问题Q遗留代码与泛型代码之间如何交互Q这个问题含有两个部分:在泛型代码内部用遗留代码;在遗留代码内部用泛型代码?br />
作ؓ一个例子,假设你想使用包com.Fooblibar.widgets。分支Fooblibar.com*商用在一个资产管理系l中Q这个系l的_֍如下所C:
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 parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
Collection getParts(); // Returns a collection of Parts
}
现在Q你要添加一些新的代码ƈ使用上述API。比较好的是Q要保你一直能够用适当的实参去调用addAssemblyҎ--卻I你传入的集合对象必须是装有Part对象的集合对象。当Ӟ泛型最适合做这些了Q?br />
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>。这是正的Q但是什么呢Q毕竟,大部分的Collection是不能包含Part对象的,因ؓ一般来_~译器无法知道该 Collection所表示的是哪种对象的集合对象?br />
在合适的泛型代码中,Collection一直跟随着一个类型参数。当一个像Collectionq样的泛型类型在被用时没有提供cd参数Q就被称之ؓ原生cd(Raw Type)?br />
大多Ch的每一直觉认ؓCollection是Collection<Object>。然而,按我们之前所说的Q在需要Collection<Object>的地方用Collection<Part>q不是安全的?br />
但请{等Q那也不对!x对getParts对象的调用,它要q回一个Collection对象(实际上是一个引用变?。然后这个对象被赋于变量kQk?Collection<Part>cd。如果调用该Ҏ而返回的l果是一个Collection<?>对象Q该赋值操作也生错误?br />
事实上,该赋值操作是合法的,它会生一个未查的警告。这个警告是必要的,因ؓ事实上编译器q不能保证它的正性。我们没办法?getAssemblyҎ中的遗留代码以保证返回的集合对象Part对象的集合。被用于该代码的cd是CollectionQ能够合法的向这U?Collection中插入Q何类型的对象?br />
那么q还应该是一个错误吗Q就理论上而言Q是的;但就实际上而言Q如果泛型代码是Z调用遗留代码Q那么就不得不允怺。对于你Q一个程序员Q会对这U情冉|到满意的Q赋值是安全的,因ؓgetAssermblyҎ的规则告诉我们它q回q回的是 Part对象的CollectionQ即使该Ҏ的签名ƈ没有表明q一炏V?br />
所以原生类型非常像通配W类型,但它们不会被做严格的cd查。这是经q深思熟虑之后的l果Q是Z允许泛型代码能够与之前已存在的代码交互用?br />
用泛型代码调用遗留代码是天生危险的;一旦你在泛型代码中混合了非泛型的遗留代码,那么泛型cdpȝ通常都无法提供完全的保证。然而,q仍然比你不使用泛型要好些。至你知道最l这些代码是一致的?br />
到那儿已经有了很多的非泛型代码Q然后又有了泛型代码的时候,那么无法避免的情况就是不得不混合它们?br />
如果你发C必须混合使用遗留代码和泛型代码,请密切注意未查的警告。要谨慎地思考你如何再才能证明那些被l出了危险警告的代码是安全的?br />
当你l箋犯错误,且代码造成的警告确实不是类型安全的Q什么事情将发生呢?让我们看看这L一U情c在q个处理q程中,我们观察编译器所做的事情?br />
擦除和翻?/span>
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();
}
此处Q我们已l别名化了String的List和一个普通的老版的List。我们向q个List xs插入一个Integer对象Qƈ试图抽取一个String对象。这昄是错的。如果我们忽略警告ƈ试执行q段代码Q它在我们试图使用错误cd的地方上p|?br />
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // run time error
}
当我们从q个List中抽取一个元素,q试囑ְ它当作String对象而把它{换成StringӞ我们得C个ClassCastException的异常。完全相同的情况也发生在了loopholeҎ的泛型版中?br />
q种情况的原因就是泛型是由Java~译器作ZU叫?擦除(Erasure)"的最前到后的机制实现的。你(几乎)可以把它惛_ZU?源代码对源代?(source-to-source)的翻译,q就是ؓ何loophole的泛型版被{换成了非泛型版了?br />
l果QJava虚拟机的cd安全和完整性再也不处于危险中了Q甚臛_遇到到未查的警告时也一栗?br />
基本圎ͼErasure去除(或者说"擦除")了所有的泛型信息。所有的在角括号中的cd信息都被抛弃了,所以,如像List<String> q样的参数化cd被{化成了List。所有保持对cd变量使用的地斚w被类型变量的高层限度cd(一般就是Object)替换了。ƈ且,无论何时产生的结果都不是cd正确的,一个向适当的类型的强制cd转换被插入了其中?br />
对Erasure的全部细节的描述出了本教程的范_但我们给出的单描q离真实情况q不太远。了解一些这斚w的知识是有益的,特别是如果你惛_一些更加老练的泛型应用,如把已有的API转换C用泛型时(详见"使用泛型转化遗留代码")Q或者只是想理解Z么它们会是这U情c?br />
在遗留代码中使用泛型代码
现在让我们思考一个颠倒的例子。想像Foolibar.com选择泛型去{化了它们的APIQ但他们的一些客LE序q没有{化。所以这些代码看h像:
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
}
客户端程序看h像:
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();
}
}
q些客户端代码是在泛型生之前写成的Q但它用了包com.Fooblibar.widgets和集合框架类库,q两者都在用泛型。客L中对泛型cd的用得它们成Z原生(Raw Type)cd?br />
代码?产生了一个未查的警告Q因Z个原生Collection被传入了一个期望是Collection<Part>出现的地方,而且~译器无法保证这个原生Collection真的是Part对象的Collection?br />
作ؓ一U可选的ҎQ你可以这些代码作为Java 1.4的源代码q行~译Q这p保证不会出现警告。但q样的话Q你不能用到JDK 5.0中Q何新的语aҎ?br />
--------------------------------------------------------------------------
注意Q?Fooblibar.com"是一个纯属虚构的公司Q目的仅仅只是ؓ了本文中的例子。Q何公司或机构、Q何健在或已故的个Z此有关的话,U属巧合?br />
译者:看来老外做事情十分}慎,对于q种"问?我们又怎么会如此郑重其事的发表一个声明呢?br />
7 良好的打?/span>
一个泛型类被它的所有应用共?br />
下面的代码片断是打印Z么呢Q?br />
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会被引诱得说是falseQ但你错了。打印的是trueQ因Z个泛型类的所有实际拥有相同的q行时类Q而不它们具体的cd参数?br />
实Q对一个类的泛型所做的事实是q个泛型cd它所有可能的cd参数都有相同的行为;相同的这个类可以被视为它有很多不同的cd?br />
同样的结果,泛型cM的静态变量和Ҏ也被该类的所有实例共享。这是Z么在一个静态方法或初始化器中、在一个静态变量的声明或初始化器中引用cd变量是非法的?br />
Cast和Instanceof
一个泛型类被它的所有实例共享的另一个隐含意义就是,如果某个实例是这个泛型类的一U特定类型的实例Q那么通常情况下请求这个类的实例是无意义的Q?br />
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // Illegal.
cM圎ͼ如下面这个强制类型{?br />
Collection<String> cstr = (Collection<String>) cs; // Unchecked warning,
会报一个未查的警告Q因不应该是q行时系l将要ؓ你检查的事情?br />
对类型变量也是如?br />
<T> T badCast(T t, Object o) {return (T) o; // Unchecked warning.
}
cd变量在运行时q不存在。这意味着在时间和I间上,它们都不可能避免地无法生作用。不q的是,q也意味着你不能可靠地在强制类型{换中使用它们?br />
数组
一个数l对象中元素的类型不会是一个类型变量或参数化的cdQ除非它是一?非受限的)通配W类型。你可以声明数组cd的元素类型是一个类型变量或参数化的cdQ但数组对象本n不行?br />
q很烦hQ但却是真的。该U束寚w免如下例子中的情冉|有必要的Q?br />
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.
如果允许有参数化cd的数l,上面的例子将会通过~译且不报Q何未查的警告Q然而会在运行时p|。我们已l知道设计泛型的主要目的是Zcd安全。特别地_Java语言被设计ؓQ如果你的整个程序用javac -source 1.5q行~译时没有报M未检查的警告Q那么这个程序就是类型安全的?br />
然而,你仍然可以用通配W数l。这儿有上面代码的两个变U。第一个变U放弃用参数化cd的数l对象和参数化类型元素。这h们ؓ了在数组外得到String对象不得不在昄C用强制类型{换?br />
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.
在第二个变种中,我们限制了数l对象的创徏Q这个数l的元素的类型被参数化了Q但仍然要将一个参数化的元素类型用于这个数l。这是合法的Q但产生一个未查的警告。确实,q段代码是不安全的,甚至会导致一个错误?br />
List<String>[] lsa = new List<?>[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.
译者:Ҏ我的试(JDK 1.5.0_11)Q?List<String>[] lsa = new List<?>[10]"q一句无法通过~译Q理׃很直?cd不匹配,不能List<?>[]转化为List< String>[]"?br />
cM圎ͼ试图创徏一个元素类型是cd变量的数l对象会D一个运行时错误Q?br />
<T> T[] makeArray(T t) {
return new T[100]; // Error.
}
因ؓcd变量在运行时q不存在Q这没有办法确定数l的实际cd?br />
围绕着q些限制的工作方法是使用了将cd面量当作q行时类型标记的机制Q该机制在下一?cd面量作ؓq行时标?中进行叙q?br />
8 cd面量作ؓq行时标?/span>
JDK 5.0的变量之一是java.lang.Class也被泛型化了。这是一个不容器c而在其它地方使用泛型机制的有例子?br />
既然ClasscL一个类型参数TQ你可能会问Q这个T代表什么?它代表这个Class对象表示的类型?br />
例如QString.class的类型是Class<String>Q而Serializable.class的类型就是Class<Serializable>。这U机制用于提高在你的反射E序中的cd安全性?br />
特别圎ͼ׃ClasscM的方法netInstance现在是返回一个TQ这样当你在使用反射机制创徏对象时能够得到更加精的cd?br />
例如Q假设你需要一个执行数据库查询的工h法,l入的是SQL字符Ԍq回的是数据库中匚w该查询语a的对象的集合?br />
一U方法就是显C地传入一个工厂对象中Q所写的代码像Q?br />
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc */
for (/* 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");
你也可以声明一个EmpInfoFactorycd支持Factory接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() { return new EmpInfo();}
}
然后像下面那样去调用?br />
select(getMyEmpInfoFactory(), "selection string");
q个解决Ҏ最l还需要:
* 在调用点使用冗长的匿名工厂类Q?br />
* 或者,为每个被使用的类型声明一个工厂类Qƈ这个工厂类的实例传递到调用点,但这U方法有点不自然?br />
可以很自然地类字面量用作工厂对象,q个工厂E后可被反射机制使用。现在这个程?不用泛型)可以写ؓQ?br />
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;
}
可是Q这不能l我们一个所期望的精类型的集合。既然Class是泛型的Q我们可以用下面的代替写法Q?br />
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;
}
上面的程序以一U类型安全的Ҏl了我们_cd的集合?br />
类字面量作行时标记的技术被认ؓ十分狡猾。例如,Z操作AnnotationQ这U技术在新API中被扩展使用了?br />
9 通配W的更多味
在本节,我们考虑一些更高的通配W用法。我们已l看了几个受限的通配W用于读取数据结构时例子。现在反q来x一个只可写的数据结构。接口Sink是这U类型的一个简单的例子Q?br />
interface Sink<T> {
flush(T t);
}
我们可以惛_它作ؓ一个范例用于下面的代码。方法writeAll被设计ؓh集合coll中的所有元素到Sink的实例snk中,q返回最后一个被h的元素?br />
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); // Illegal call.
已l写出来的,对writeAllҎ的调用是非法的,׃无法推断出有效的cd实参QString或Object都不是T的合适类型,因ؓCollection的元素和Sink必须是相同的cd?br />
我们可以通过修改writeAll的方法签名来修正q个错误Q如下所C,使用了通配W:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s); // Call is OK, but wrong return type.
该调用是合法的,但赋值是错的Q是׃q回cd被推断成了ObjectQ因为T匚ws的类型,但s的类型是Object?br />
该解x案用了一U我们尚未见q的受限通配WŞ式:有一个较低限度的通配W。语?? super T"表示未知cd是T的超cd(或者是T本nQ记住,类型关pLҎ的)?br />
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // Yes!
使用了这U语法,Ҏ的调用就是合法的Qƈ且被推断的类型正如所愿是String?br />
现在让我们{向更为实际的例子。java.util.TreeSet<E>表示了一个排序了的以cd为E的对象作为元素的树。构造一?TreeSet对象的方法之一是传递一个Comparator对象l这个构造器。该Comparator对象被用于Ҏ期望的规则对TreeSet中的元素q行排序?br />
TreeSet(Comparator<E> c)
Comparator接口是必ȝQ?br />
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们惛_Z个TreeSet<String>对象Qƈ传入一个合适的比较器对象。我们就需要一个能比较String?Comparator对象Q一个Comparator<String>可以做刎ͼ但一个Comparator<Object> 对象也能做到。然而,我们不能调用上面Comparator<Object>所提供的构造器?br />
TreeSet(Comparator<? super E> c)
上述代码允许适用的比较器被用?br />
作ؓ最后一个低位受限通配W的例子Q让我们看看Collections.maxҎQ该Ҏq回一个集合中的极大元素。ؓ了让max文g能够工作Q集合中所有的传入该集合的元素都必dCComparable接口。此外,它们怺之间必须是可被比较的?br />
在第一ơ尝试创个方法后有如下结果:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
卻Iq个Ҏ有一个某cdT的集合对象,T的实例之间可以进行比较,该方法ƈq回一个该cd的元素。然而,q个E序实现h太受限制了。看看是Z么,考虑一个对象,它能与Q意对象进行比较:
class Foo implements Comparable<Object> {
...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // Should work.
Collection cf中的每个元素都能与该集合中的其它元素q行比较Q因为每个这L元素都是一个Foo的实例,而Foo的实例能够与L对象q行比较Q则与另一个Foo 对象比较那就更没问题了。然而,使用前面的方法签名,我们可以发现上面Ҏ法max的调用会被拒l。被推断出的cd必须是FooQ但Fooq没有实?Comparable<Foo>?br />
没有必要_地要求T与它自己的实例进行比较。所有被要求的是T的实例能够与它的某个类型的实例q行比较。这p我们有了如下代码Q?br />
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
注意到Collections.max真实的方法签名更难以理解。我们将在下一?遗留代码{化到使用泛型"中再讲述它。这个适用于几乎Q何一?Comprarable应用的理论是打算能用于Q意的cdQ你L想用Comprarable<? super T>?br />
一般地Q如果你的API只是类型参数T作ؓcd变量使用Q那应该利于低位受限通配W?? super T)。相反地Q如果这个API只需q回TQ你p使用高位受限通配W?? extends T)以给q个API的客LE序更大的灵zL?br />
通配W捕?br />
到目前ؓ此,下面的程序应该更清晰些:
Set<?> unknownSet = new HashSet<String>();
...
/** Add an element t to a Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
但下面的调用是非法的?br />
addToSet(unknownSet, "abc"); // Illegal.
传入该方法的一个精的Set是一个String的Setq没有媄响;问题在于作ؓ实参传入表达式的是一个未知类型的SetQ这q不能保证它一定就是String或其它Q何特定类型的Set?br />
现在考虑下面的代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
看v来它应该不被允许Q然而,看看q个Ҏ的调用,它确实是安全的而可以允许这么做。毕竟,unmodifiableSetҎ可用于Q何类型的SetQ而不这个Set中的元素的类型?br />
因ؓq种情况发生地相Ҏ较频J,所以有一个特D的规则允许q些在一个非常特D的环境中的代码是合法的Q在q个环境中这些代码被证明是安全的。这个名?通配W捕?的规则允许编译器通配W的未知cd作ؓcd实参推断到泛型方法中?br />
10 遗留代码{化ؓ使用泛型
早先Q我们展CZ新、老代码之间如何交互。现在是时候看?泛型?老代码这个困隄问题了?br />
如果你决定将老代码{换成使用泛型Q你需要仔l考虑如何M改你的API?br />
你需要确定泛型化的API不会造成q度的限Ӟ它必能l箋地支持API原先的功能。再ơ考虑一些来自于java.util.Collection中的例子。没有用泛型的API看v来像Q?br />
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
一U自然的泛型化尝试可能像下面那样Q?br />
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
肯定是类型安全的了,但它q没有实现该API之前的功能。containsAllҎ用于M引入的集合对象,如果引入的集合真C包含E的实例时Q该Ҏ才会成功。但是:
* 引入集合的静态类型可能有所不同Q或许是因ؓ调用者不知道传入的集合对象的准确cdQ或者可能是因ؓ它是一个Collection<S>Q而S是E的子cd?br />
* 能够合法C用一个不同的cd的集合调用containsAllҎ则最为理想了。这U方法应该能工作Qƈ返回false?br />
在这个例子中的addAllҎQ我们应该能够加入由M由E的子cd的实例组成的集合对象。我们在"泛型Ҏ"q一节中已经看过了如何正地处理此类情况?br />
你也需要保证修改后的API要保持与老的客户端程序的二进制兼Ҏ。这暗C着"擦除"后的API必须与以前的非泛型化API相同。在大部分例子中Q这自然会引用争吵,但也有一些精妙的例子。我们将试我们已经遇到q的最_֦例子中的一个,即Collections.max()Ҏ。根据我们在"通配W的更多乐趣"一节所看到的,一个模p的maxҎ{是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
除了擦除后的{之外Q这些都很好Q?br />
public static Comparable max(Collection coll)
q与max之前的方法签名不同:
public static Object max(Collection coll)
当然可以q样指定maxҎ的签名,但这没有什么用。所有老的调用Collections.maxҎ的二q制class文g都依赖于q回cd为Object的方法的{?br />
通过昄地在限度中ؓ形式cd参数T指定一个超c,我们能够强制q个擦除产生不同的结果?br />
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
q是一个单个类型参数有多个限度的例子,使用语法"T1 & T2 ... & Tn"。有多个限度的类型变量是被认为是限度中所有类型的一个子cd。当使用多限度时Q限度中W一个被提及的类型将作ؓ该类型变量被擦除后的cd?br />
最后,我们应该回想到maxҎ只需从输入的Collection中进行读取操作,所以这适合于T的Q何子cd的集合?br />
q就把我们带入到JDK中该Ҏ的真实签名中Q?br />
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在实践中产生如此晦ӆ的应用是十分|见的,但是当{换现有APIӞ专家型的cd设计者们应该要准备着去进行非常细致地地思考?br />
另一个问题需要密切关注的是"协变q回"Q即在一个子cd中精gq回cd。你不需要在老的API中用这个特性。ؓ了找到原因,让我们看一个例子?br />
假设你原先的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.
}
Z利用"协变q回"Q你它修改为:
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.
}
现在假设有一个像下面那样写的你代码的W三方客LE序Q?br />
public class Baz extends Bar {
public Foo create() {
...
} // Actually creates a Baz.
}
Java 虚拟Z直接支持有着不同q回cd的方法的覆盖Q该Ҏ由~译器支持。因此,除非Bazc被重新~译Q否则它不能正常地覆盖Bar的createҎ。另外,Baz不得不被修改,因ؓq些代码如前面所写的那样被拒l?-Baz中的createҎq回cdq不是Bar中createҎq回cd的子cd?br />
译者:Ҏ我的试(JDK 1.5.0_11)QBazcM的createҎ无法通过~译Q理由就是Baz.createҎ与Bar.createҎ的返回不兼容Q返回类型须是BarQ而不是Foo?br />
致谢
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe和Philip Wadler教程提供了材料?br />
感谢David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata和Kresten Krab Thorup教程的早期版本所提出的富有h值的反馈。向我忘记列出来的每个h道歉?/span>