《Generics in the Java Programming Language》译文

来源:互联网 发布:sqlserver 行为日志 编辑:程序博客网 时间:2024/05/16 13:40

Java中的泛型

                                于2004年1月                            Gilad Bracha 谷歌工程师

1 介绍

JDK1.5在原先的版本上增加了几项新的特性,泛型就是其中的一项。
这篇文章可以让你对泛型有个更加深刻的了解。你可能曾经了解过一些和它类似的概念,比如C++的模板,如果是这样的话,那你可以通过这篇文章很快的理解它们之间的相同点和不同点。如果你之前完全没有接触过类似的概念,因祸得福,你可以从零开始且无误的学习泛型。
泛型允许你对类型进行抽象,最常见的例子就是Collection继承树上的容器类型。
下面是一个典型的使用案例:

List myIntList = new LinkedList();//1myIntList.add(new Integer(0));//2Integer x = (Integer) myIntList.iterator().next();//3

第三行的写法让人感到一些不舒服。通常情况下,程序员可能知道一个特定的list中存放了什么类型的数据,但是编译器只知道这个iterator返回的是一个Object,所以类型的转换是格外重要的,但是频繁的类型转换使代码冗余且难以阅读。
当然,这种类型的转换不仅造成混乱,而且容易触发运行时异常,就像ClassCastException。
如果程序员能明确表达自己的需求,并且可以标记一个列表使它专门用于存储某种特定类型的数据又会怎么样呢?这也是泛型的核心问题。下面是使用了泛型的代码片段:

List<Integer> myIntList = new LinkedList<Integer>();//1’myIntList.add(new Integer(0));//2’Integer x = myIntList.iterator().next();//3’

注意变量myIntList的类型声明,List表明myIntList不是一个可以装任何类型数据的List,而是一个专门用于Integer的List。我们可以说myIntList是一个携带类型参数的泛型接口,在上面的例子中,这个类型参数是Integer,我们在构建这个List对象的时候就指定了这个类型参数。
另一件值得注意的事是3’的转型。
现在,你可能认为自己将会从泥沼(繁琐的代码)中脱身而出。我们在代码1’处使用了Integer这个类型参数,这取代了代码3处的转型。这中间有很大的不同,编译器会在编译期检查类型的正确性。当我们声明变量myIntList为List时,就决定了不管何时何地使用它,它持有的类型都是正确的,这种正确性由编译器来保证。与此形成鲜明对比的是,转型只能表明在代码的单个点上,程序员保证它的正确性。
泛型的这种效果,尤其在大型的项目中,极大提高了代码的稳定性和健壮性。

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的正式的类型参数。
通过泛型使用类型参数,比使用普通的类型声明要优雅得多(虽然这其中有很多重要的限制,见第七节)。
在第一节中,我们对泛型的List如List进行了调用。在所有的调用中,类型形式参数比如E都被替换成具体的类型,比如Integer。
你可能觉得List是List的一个版本,其中的E被一致地替换成Integer:

public interface IntegerList{    void add(Integer x);    Iterator<Integer> iterator();}

这种感觉是很有帮助的,但是同样是错误的。
泛型的声明不是通过这种方式来实现的,源代码、二进制文件、磁盘或者内存中都不存在这种形式相近参数不同的大量代码。如果你是一个C++程序员,你会发现这和C++的模板存在很大的不同。
泛型java文件被编译之后,和不使用泛型的class文件别无二致。
在方法和构造器中,类型参数和普通参数在使用上是相近的。一个方法的形式参数代表了这个方法能操作的数据的类型,泛型方法的类型形式参数也一样。当一个普通方法被调用时,形参会被传入的实参替换,同样的,当一个泛型方法被调用时,类型形式参数就会被传入的类型具体参数替代。
在类型参数的命名规则上有一些约定。我们建议使用单个字符,最好不要使用小写的字母,这有助于区分类型参数和普通的类名和接口名。一些容器类通常使用E这个字母,上面的例子就是如此。在后面的例子中我们会看到更多的约定。

3 泛型以及子类型

测试一下我们对泛型的了解程度,下面的代码片段是正确的吗?

List<String> ls = new ArrayList<String>();//1List<Object> lo = ls;//2

代码的第一行肯定是合法的,问题的关键是第二行,它衍生出了一个问题:一个String泛型类型的List是一个Object泛型类型的List吗?大部分人可能会脱口而出:“当然!”。
好吧,再看看下面的代码:

lo.add(new Object());//3String s = ls.get(0);//4:尝试把一个Object类型的对象转为String类型

lo和ls实际上都是一个ArrayList对象的别名,通过lo,我们强制把一个Object类型的对象塞入到一个实际上专门放置String类型对象的List中。这使得ls中放置的不只是String类型的对象了,当我们尝试从中取出东西时,我们可能会得到一些“惊喜”。
Java编译器会尽量防止这样的代码出现,第二行的代码会出现编译期错误。
一般情况下,假设G是一个泛型类型声明,Foo是Bar的子类,但这不代表G是G的子类。这可能是关于泛型你最难理解的点了,因为这和你的直觉认知相悖。
产生这种直觉的原因是我们常常假设容器是不会改变的。
比如,如果要机动车部门向人口普查局提供一份司机名单,这似乎是合理的。我们认为一个List就是一个List,假设司机Driver是Person的一个子类。我们实际上给委员会的是一份司机注册表的拷贝,否则,人口委员会就可以往表里加非司机的人的名字,就像腐败的DMV(车辆管理局)一样。
为了处理这种情况,在泛型类型上想得更多点是很有帮助的。到目前为止,我们看到的规则都是很严格的。

4 通配符

如果要输出一个集合中的所有元素,你可能想到下面的代码:

void printCollection(Collection c){    Iterator i = c.iterator();    for(int k = 0;k<c.size();k++){        System.out.println(i.next());    }}

下面的代码使用泛型是为了一个单纯的目的(同时使用了增强的for循环):

void printCollection(Collection<Object> c){    for(Object e : c){        System.out.println(e);    }}

问题在于新版本的代码(指使用了泛型)并没有比老版本的更加有用,老版本可以使用任何类型的Collection作为参数,而新版本的只能使用Collection作参数,之前已经演示了Collection并不是任何类型的Collection的父类。
那什么才是任何类型的Collection的父类呢?Java中使用Collection

void printCollection(Collection<?> c){    for(Object e:c){        System.out.println(e);    }}

现在,我们就可以认为它是任意类型的Collection,我们可以从c中获取元素并将它们赋给Object的引用,这始终是安全的,因为不管元素的实际类型是什么,它们都是Object的子类。但是像Collection

Collection<?> c = new ArrayList<String>();c.add(new Object());//编译期出错

我们不知道c中的具体元素类型是什么,因此我们不能向其中添加元素。add()方法使用Collection的元素类型E作为参数类型。通配符?代表的是未知的类型,我们传递给add方法的参数类型必须是这个未知类型的子类,但是我们并不知道这个未知类型是什么,所以我们什么都不能传递给add方法。null是例外,因为null是任何对象类型的共有成员,可以向Collection

4.1 有界通配符

假如我们要做一个可以画矩形、圆形等图形的应用程序,为了表示这些形状,我们必须定义一个如下的类继承结构:

public abstract class Shape{    public abstract void draw(Canvas c);}public class Circle extends Shape{    private int x,y,radius;    public abstract void draw(Canvas c){...};}public class Rectangle extends Shape{    private int x,y,width,height;    public abstract void draw(Canvas c){...};}

这些类可以在布上作画:

public class Canvas{    public void draw(Shape s){        s.draw(this);    }}

作画需要一定数量的图形,假设它们都被存放在一个list中,如果有一个方法能同时将这些图形全部画出一定很方便:

public void drawAll(List<Shape> shapes){    for(Shape s:shapes){        s.draw(this);    }}   

现在,drawAll方法只能用于Shape的list,它不能用于其他的list,比如List,这不让人感到愉快,因为Circle,Rectangle也属于Shape,我们真正想要的是可以接受任何Shape类型的list的方法:

public void drawAll(List<? extends Shape> shapes){...}

有一点小的改动,把List改为List

public void addRectangle(List<? extends Shape> shapes){    shapes.add(0,new Rectangle());//编译期报错}

你应该要明白上面的代码为什么不合法,add方法的第二个参数是? extends Shape-Shape的一个未知的子类。我们不清楚它具体是什么类型,它可能是Rectengle的父类并且是Shape的子类,这种情况下是合法的,但它也可能是Rectangle的一个子类或者是和Rectangle处于同一继承层次的类,这样就不合法了,你不能用子类的引用去接收父类的对象。所以这里的参数传递是不安全的。
从DMV(车辆管理局)向人口普查局递交司机信息就可以用到上界通配符,假设人的姓名(用String修饰)和人(比如Person以及Person的子类Driver)是对应的,就像Map

public class Census{    public static void addRegistry(Map<String,? extends Person> registry){...}    ...    Map<String,Driver> allDrivers = ...;    Census.addRegistry(allDrivers); }

5 泛型方法

假设你想写一个方法,用来实现:把一个数组中的所有元素移到一个集合中。下面是第一个尝试:

static void fromArrayToCollection(Object[] a,Collection<?> c){    for(Object o:a){        c.add(o);//编译期报错    }}

到现在为止,你已经知道了如何避免一些初学者的错误,比如使用Collection作为参数,你也知道了不能向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是ObjectString[] sa = new String[100];Collection<String> cs = new ArrayList<String>();fromArrayToCollection(sa,cs);//推断T是StringfromArrayToCollection(sa,co);//推断T是ObjectInteger[] ia = new Integer[100];Float[] fa = new Float[100];Number[] na = new Number[100];Collection<Number> cn = new ArrayList<Number>();fromArrayToCollection(ia,cn);//推断T是NumberfromArrayToCollection(fa,cn);//推断T是NumberfromArrayToCollection(na,cn);//推断T是NumberfromArrayToCollection(na,co);//推断T是ObjectfromArrayToCollection(na,cs);//编译期错误

注意,我们不需要传递一个具体的类型参数给泛型方法,编译器会帮我们推断出类型参数。通常编译器会推断出使得方法可以被正确调用的最具体的类型参数。
出现的问题是:我们什么情况下使用泛型方法,什么情况下使用通配符呢?为了找出这个问题的答案,举一些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 <T extends E> boolean addAll(Collection<T> c);//类型变量也可以有界限}

然而,在containsAll和addAll方法中,类型参数T只被使用了一次。返回类型不依赖类型参数,方法的其它参数同样也不依赖(本例子中,只有一个参数)。它唯一的作用是为了适应于不用参数类型的方法调用,如果是这样的话,应该使用通配符,设计通配符是为了支持灵活的子类型化,这是我们这里想尽力表达的。
泛型方法允许类型参数和其它参数或返回类型有依赖,如果不存在这种依赖,那最好不要使用泛型方法。
可以在一个泛型方法中使用通配符:

class Collections{    public static <T> void copy(List<T> dest,List<? extends T> src){...}}

注意两个参数的类型之间的依赖关系,任何从src到dest的对象都必须是T或者T的某个子类对象,所以src的元素类型可以是T的任何子类,但是我们并不关心具体是哪个。copy方法的方法签名表明了使用类型参数的依赖性。
当然也可以不使用通配符:

class Collections{    public static <T,S extends T> void copy(List<T> dest,List<S> src){...}}

这是可行的,T一方面被应用在dest中,一方面作为S的上界,但是S只是在src中被使用了一次,也没有其它东西依赖于它了,这可以作为建议我们使用通配符而不是泛型的信号。使用通配符比再定义一个泛型参数更加简洁清晰,尽可能将其作为首选。
通配符同样可以在方法签名之外使用,可以作为字段、本地变量或数组的类型,例子如下:
回到画图形的例子,假设想对画图形的请求做一个历史记录,我们可以在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 和遗留代码打交道

到目前为止,所有的例子都是在最理想的环境中完成的,使用支持泛型的最新JDK。
但是,真实情况却不是这样,现实中有大量的代码都是在老版本JDK基础上完成的,它们不能在一夜之间就转换成最新版本。
第十章中,我们将会讨论如何将你的老代码转换成泛型版本,在本节中,我们将会专注于一个简单的问题:遗留代码怎么和泛型代码交互?这个问题分为两部分:在泛型代码中使用遗留代码,在遗留代码中使用泛型代码。

6.1 在泛型代码中使用遗留代码

如何在你的代码中使用遗留代码,同时还享受泛型带来的便利呢?
假设你想使用包com.Fooblibar.widgets的代码,实现库存控制,下面是其中的核心代码:

package com.Fooblibar.widgets;public interface Part{...}public class Inventory{    /**     *库存增加,增加集合的元素都要实现Part接口     */    public static void addAssembly(String name,Collections parts){...}    public static Assembly getAssembly(String name){...}}public interface Assembly{    Collection getParts();//返回Parts的一个集合}

现在,你想在这个API的基础上添加新的代码。如果在调用addAssembly()方法的时候可以保证传入的参数是正确的,那是极好的。你向addAssembly()方法传递的必须是Part对象的集合,泛型就是为此而生的:

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类型的,代码同样可以运行,为什么呢?毕竟大部分的Collection并不包含Part类型的对象,一般情况下,编译器也没办法知道Collection中存放的对象的类型。
在泛型代码中,Collection常常会伴随一个类型参数,如果这个参数不存在,那我们称这个Collection为一个raw type(原生类型)。
大部分人第一次看到Collection会把它当成Collection,然而,就像我们说过的,把一个Collection类型的参数传入需要Collection类型参数的地方是不安全的。更确切的说,Collection代表了一种未知类型,就像Collection

6.2 擦除和转化

public String loophole(Integer x) {        List<String> ys = new LinkedList<String>();        List xs = ys;        xs.add(x);        return ys.iterator().next();}

在这里,我们将一个装String的List对象赋给List引用,向其中插入一个Integer对象,并试图从中返回一个String。这显然是不正确的,如果我们忽略警告并执行代码,代码就会在类型转换的时候出错。在运行期,上面的代码和下面的代码并无二致:

public String loophole(Integer x) {        List ys = new LinkedList();        List xs = ys;        xs.add(x);        return (String)ys.iterator().next();//run time error}

当我们从ys中返回一个对象,将其看成一个String对象并试图类型转换为String,那我们就会得到一个ClassCastException。这和泛型版本的loophole()的情况是一样的。
Java编译器实现泛型的方式是一种前后转换的方式,名为“擦除”。可以认为这是一种源代码到源代码的转换,借此泛型的loophole()可以转化为非泛型的loophole()。
从此可以看出,不管代码有没有非受检警告,Java虚拟机都能正确运行。
擦除会摒除所有的泛型信息,在尖括号中的信息会被抛弃,比如List会被转为List,其中持有的对象都会被当成像Object一样的上界类型对象。不管返回对象的类型是否正确,类型转换都会发生,就像非泛型的loophole()方法的最后一行一样。
关于擦除的全部细节比本文章讲述的要丰富得多,但是上面的简单描述离本质也不是很远。了解这些对你做一些比如将现存的API转为泛型版本之类的复杂的事有帮助,或者你只想了解代码为什么会这样运行。

6.3 在遗留代码中使用泛型代码

现在让我们考虑相反的情况,如果Fooblibar.com升级了他们的API,将其转化为泛型版本,但是一些使用他们API的客户还没有修改自己的客户端代码,就像下面这样:

package com.Fooblibar.widgets;public interface Part{...}public class Inventory{    /**     * 向存货清单数据库中增加一种新的配件。     * 通过给定配件的名称和配件的集合,所有的配件类必须实现Part接口。     */    public static void addAssembly(String name,Collection<Part> parts) {...}    public static Assembly getAssembly(String name) {...}}public interface Assembly{    Collection<Part> getParts();//返回配件的集合}客户端的代码如下: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和Collection类库现在都使用了泛型。客户端所有使用泛型类型的声明都是原生类型。
main方法第一行会产生非受检警告,因为编译器希望此处是个Part类型参数的Collection,编译器不能保证这个原生的Collection是个Part类型参数的Collection。
当然你可以使用旧的jdk版本比如1.4来编译代码,这样警告就不会产生了,但是同时你也不能使用1.5中的新特性了。

7 注意要点

7.1 所有泛型类型共享同一个Class对象

下面的代码段的输出是什么?

List<String> l1 = new ArrayList<String>();List<Integer> l1 = new ArrayList<Integer>();System.out.println(l1.getClass()==l2.getClass());

你可能认为会输出false,但是结果是true,泛型类在运行期共享同一个Class对象,和参数类型无关。
事实上,不管类型参数是什么,类都拥有相似的行为,可以这么认为,类能拥有不同种类的类型参数。
静态变量和方法能被所有的实例共享,这也是不允许在静态方法,静态初始化块中使用类型参数声明的原因,当然类型参数声明的变量也不能使用static修饰符。

7.2 类型转换和instance of

判断一个实例是否是某个带有泛型参数的类的实例是没有意义的:

Collection cs = new ArrayList<String>();if(cs instanceof Collection<String>){...}//illegal

同样地,下面的类型转换:

Collection<String> cstr = (Collection<String>)cs;//unchecked warning

这里产生一个非受检警告,在运行期虚拟机是不会为你检查的。
类型变量同样如此:

<T> T badCast(T t,Object o) {    return (T)o;//unchecked warning}

类型参数在运行期是不存在的,这代表它们不占用系统性能和内存,但是同时也意味着你不能可靠地将它们用于类型转换。

7.3 数组

数组的类型不可以是类型变量,除非是采用通配符的方式。你可以声明数组包含的元素是类型变量,但是数组对象不可以。
这确实很烦人,应该避免下面情况的发生:

List<String>[] lsa = new List<String>[10];//not really allowedObject 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);//run-time error - ClassCastExcpetion

如果数组允许泛型参数,那上面的代码在编译期就不会产生任何警告,直到运行期报错。设计泛型主要是为了类型安全。如果你的程序在编译期没有出现非受检警告,那可以说是类型安全的。
然而,你还是可能会使用通配符数组这样的类型。下面有两个版本的代码,第一个版本放弃使用泛型数组,这样的结果是我们从数组中取出对象时必须进行显式转换。

List<?>[] lsa = new List<?>[10];//ok,array of unbounded wildcard typeObject o = lsa;Object[] oa = (Object[])o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));oa[1] = li;//correctString s = (String)lsa[1].get(0);//run-time error - but cast is explicit

在另一个版本中,我们不使用参数类型来创建一个数组,但是使用其作为引用的声明。这是合法的,但是会产生一个非受检警告。这代码确实是不安全的,最终也会产生错误。(注:在后面的jdk版本中,已经不允许这么做了)

List<String>[] lsa = new List<?>[10];//unchecked waring-this is unsafeObject o = lsa;Object[] oa = (Object[])o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));oa[1] = li;//correctString s = lsa[1].get(0);//run-time error - but we were warned

同样地,试图使用类型变量作为数组元素类型来创建一个数组会造成编译器错误:

<T> T[] makeArray(T t) {    return new T[100];//error}

这是因为类型变量在运行期是不存在的,没有办法确定数组元素的真正类型。
在这种种限制下,使用类的字面量作为运行期的类型标识是一种不错的方式,见第8章。

8 使用类的字面量作为运行期的类型标识

JDK1.5的java.lang.Class使用泛型来实现了,可以将其当做一个容器类以外的类泛型化的例子。
现在Class类上有一个类型参数T,你也许会问,这个T代表什么?它代表这个Class对象代表的类型。
比如,String.class的类型是Class,Serializable.class的类型是Class,这可以提高你反射代码的类型安全性。
因为新版本的Class类的newInstance()方法现在返回T,所以你在创建实例时可以更加深思熟虑。
比如,你想写一个数据库查询的方法,输入String类型的SQL语句,并且返回符合要求的结果的集合。
一种方式是使用工厂对象,代码如下:

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"};

当然你也可以声明一个实现Factory接口的EmpInfoFactory类。

class EmpInfoFactory implements Factory<EmpInfo>{    public EmpInfo make() {        return new EmpInfo();    }}

然后使用下面的代码来调用:

select(getMyEmpInfoFactory(),"selection string");

代码的不足之处是:
·使用匿名类会使代码变得冗长。
·声明类的话则每种类型都需要构造一个类,这让人觉得不自然。
如果使用可以用于反射的类字面量作为工厂对象则让人感到自然很多:

Collection emps = sqlUtility.select(EmpInfo.class,"select * from emps");...public static Collection select(Class c,String sqlStatement) {    Collection<E> result = new ArrayList();    /*run sql query using jdbc*/    for(/*iterate over jdbc resluts*/) {        Object item = c.newInstance();        /*use reflection and set all of item's from sql results*/        result.add(item);    }    return result;}

上述的代码不能返回一个特定元素类型的Collection。如果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;}

这样就返回一个安全的特定元素类型的Collection。
使用类字面量是一种非常有用的技巧,比如广泛用于在一些新的API中操作注解。

9 通配符其它有趣的部分

在这一章中,我们将发现一些通配符的比较好的用法。在这之前我们已经看过一些限界通配符和从数据结构中读数据结合使用的例子,现在相反,只写的数据结构的情况。
Sink接口就是一个这样的例子:

interface Sink<T>{    flush(T t);}

下面是代码示范,writeAll()方法的作用是使用实现了Sink接口的snk对象来刷新(flush)Collection对象coll的每个元素,然后返回最后一个被刷新的元素。

public static <T> T writeAll(Collection<T> coll,Sink<T> snk) {    T last;    for(T t:coll) {        last = t;        sink.flush(last);    }    return last;}...Sink<Object> s;Collection<String> cs;String str = writeAll(cs,s);//illegal call

上面代码使用writeAll()方法的方式是非法的,因为不能推断出正确有效的参数。不管是String还是Object都不是正确的,因为Collection的元素和Sink参数的类型必须是一样的。
我们可以修改writeAll()方法的签名,使用通配符:

public static <T> T writeAll(Collection<? extends T> coll,Sink<T> snk) {...}...String str = writeAll(cs,s);//call ok,but wrong       return type

方法的调用是合法的,但是赋值却是错误的,因为返回类型应该是Object,Object对应的是T。
为此我们可以使用一种我们还未见过的下界通配符,? super T代表一种未知的T的超类,? extends T代表一种未知的T的子类。

public static <T> T writeAll(Collection<T> coll,Sink<? super T> snk) {...}...String str = writeAll(cs,s);//Yes!

通过使用下界通配符,方法调用是正确的,T是String。
现在让我们转向更实用的例子。java.util.TreeSet表示一颗E类型元素排序而成的树,构造一个TreeSet的方法是向构造器中传递一个Comparator对象,这个Comparator对象被用来对元素进行排序。

TreeSet(Comparator<E> c)Comparator接口如下:interface Comparator<T>{    int compare(T fst,T snd);}

假设我们想构造一个TreeSet并且传一个合适的Comparator实例。我们需要可以比较String对象的Comparator,比如Comparator,但同时一个Comparator也能运行良好,TreeSet(Comparator c)不允许我们将Comparator对象传入构造器,我们可以使用下界通配符来修改这个构造器:

TreeSet<Comparator<? super E> c)

这样就可以使用所有的合适的Comparator了。
最后让我们看一个下界通配符的例子,Collections.max()方法可以返回传入的集合的最大值。
为了让max()方法可以正常运行,集合中的元素必须都实现Comparable接口,此外,元素之间必须是两两可比较的。
首先尝试使用泛型化的参数:

public static <T extends Comparable<T>> T max(Collection<T> coll)

coll里的T类型的对象可以相互比较,这感觉有点受限制。
想想如果存在一种类型,它可以和任何对象进行比较。

class Foo implements Comparable<Object>{...}...Collection<Foo> cf = ...;Collections.max(cf);//should work

cf中的任何两个元素都可以进行比较,因为cf中元素的类型是Foo,Foo可以和任何对象进行比较,尤其是Foo和Foo之间的比较。然而,上面的方法并不能很好的调用,因为这里的T是Foo,而Foo必须实现Comparable。
T不一定必须只能和自己进行比较,但是T要和自己的超类型进行比较。

public static <T extends Comparable<? super T>> T max(Collection<T> coll)

一般来说,如果你只使用T作为参数,最好使用下界通配符(? super T),相反地,如果你想返回T类型的对象,最好使用上界通配符(? extends T)。

9.1 通配符匹配

Set<?> unknowSet = new HashSet<String>();/*Add an element t to a Set s*/public static <T> void addToSet(Set<T> s,T t) {...}

下面的方法调用是非法的:

addToSet(unknowSet,"abc");//illegal

虽然实际传入方法的是HashSet对象,但是传递的表达式是未知类型的,并不能保证这个未知类型是String。

class Collections{        public static <T> Set<T> unmodifiableSet(Set<T> set){...}}...Set<?> s = Collections.unmodifiableSet(unknowSet);//this works!Why?

看上去这不被允许,然而,方法的调用确实是安全的。毕竟,unmodifiableSet()方法不管把什么参数类型的set作为参数都可以运行。
因为这种情况相对出现地比较频繁,所以允许代码这样写,而且也证明代码这样写是正确的。这个规定,叫作通配符匹配,使得编译器允许把一个通配符当做泛型方法的类型参数。

10 将遗留代码转为泛型版本

之前我们讨论的都是遗留代码和泛型代码之间的互操作,现在让我们着眼于将旧代码泛型化。
如果你想使用泛型来改造旧代码,那你要万分小心。
你必须要确定修改后的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类型的,但是:
·如果传入的Collection对象是静态类型的那可能不同,或许是因为调用者不知道它的确切类型,或者传入的可能是一个Collection类型的,S是E的子类型。
·如果传入的是不同类型的Collection那也应该是合理的,方法应该执行并返回false。
addAll()方法应该可以加入E或者E子类型的集合,怎么处理这种情况我们已经在第五章中讨论过了。
同时你也要保证改造后的API要和原先的API保持互换性,这说明改造后的API擦除后和原先的API是一样的。大多数情况下,这很自然的发生,但是也有一些例外。我们将会拿我们遇到过的Collections.max()方法当例子,就像我们在第九章中看到的,下面的max()貌似是可行的:

public static <T extends Comparable<? super T>> T max(Collection<T> coll)

它擦除之后就变成了:

public static Comparable max(Collection coll)

这和原先的版本擦除之后是不一样的:

public static Object max(Collection coll)

一些老的代码调用Collections.max()的时候都是把它当做返回Object来处理的。
我们可以改变这种擦除后的不同,只要为T绑定一个父类型:

public static <T extends Object & Comparable<? super T>> T max(Collection<T> coll)

这里使用了多重绑定,使用的语法是:T1&T2…&T3。类型变量是多重绑定的所有类型的子类。当使用多重绑定的时候,多重绑定中的第一个类型被当做类型变量擦除后的类型。
输入的参数是T子类的集合也是合理的,下面就是最终的JDK中的版本:

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)

例子中的情况在实际使用中是很罕见的,但是类库的设计者在转换旧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}

现在,如果有一个类Baz继承了Bar:

public class Baz extends Bar{    public Foo create() {...}//actually create a Baz}

JVM不允许重写方法的返回类型是不同的,这个特性是编译器支持的,Baz必须修改,因为Baz中的create()方法的返回类型不是Bar中的create()方法的返回类型的子类。(子类重写方法的返回类型要是父类重写方法返回类型的派生类)

11 鸣谢

    略...

★第一次翻译知识文章,不好的地方望指正。

原创粉丝点击