Java1.5泛型指南中文版(Java1.5 Generic Tutorial)

Java1.5泛型指南中文版(Java1.5 Generic Tutorial)前端

英文版pdf下载连接:http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdfjava

                                                 译者: chengchengji@163.com程序员

 

        sql

 

摘要和关键字数据库

1.       介绍express

2.       定义简单的泛型编程

3.       泛型和子类继承数组

4.       通配符(Wildcards)安全

4.1.       有限制的通配符(Bounded Wildcards)数据结构

5.       泛型方法

6.       与旧代码交互

6.1.       在泛型代码中使用老代码

6.2.       擦除和翻译(Erasure and Translation)

6.3.     在老代码中使用泛型代码

7.       要点(The Fine Print)

7.1.       一个泛型类被其全部调用共享

7.2.       转型和instanceof

7.3.       数组Arrays

8.       Class Literals as Run-time Type Tokens

9.       More fun with *

9.1.       通配符匹配(wildcard capture)

10.     泛型化老代码

11.     致谢

 

摘要和关键字

       genericstype safetype parameter(variable)formal type parameteractual type parameterwildcards(?)unknown type? extends T? super TerasuretranslationcastinstanceofarraysClass Literals as Run-time Type Tokenswildcard capturemultiple bounds(T extends T1& T2 ... & Tn)covariant returns

     


 

1.            介绍

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,而是一个IntegerList,写做: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();

}

这些都应该是很熟悉的,除了尖括号中的部分,那是接口ListIterator中的形式类型参数的声明(the declarations of the formal type parameters of the interfaces List and Iterator)

类型参数在整个类的声明中可用,几乎是全部但是使用其余普通类型的地方(可是有些重要的限制,请参考第7部分)

(原文:Type parameters can be used throughout the generic declaration, pretty much where you would use ordinary types (though there are some important restrictions; see section 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行。

这产生一个问题:

一个StringList是一个ObjectList么?大多数人的直觉是回答:固然!

好,在看下面的几行:

lo.add(new Object()); // 3

String s = ls.get(0); // 4: 试图把Object赋值给String

这里,咱们使用lo指向ls。咱们经过lo来访问ls,一个Stringlist。咱们能够插入任意对象进去。结果是ls中保存的再也不是String。当咱们试图从中取出元素的时候,会获得意外的结果。

java编译器固然会阻止这种状况的发生。第2行会致使一个编译错误。

总之,若是FooBar的一个子类型(子类或者子接口),而G是某种泛型声明,那么G<Foo>G<Bar>的子类型并不成立!!

这多是你学习泛型中最难理解的部分,由于它和你的直觉相反。

这种直觉的问题在于它假定这个集合不改变。咱们的直觉认为这些东西都不可改变。

举例来讲,若是一个交通部(DMV)提供一个驾驶员里表给人口普查局,这彷佛很合理。咱们想,一个List<Driver>是一个List<Person>,假定DriverPerson的子类型。实际上,咱们传递的是一个驾驶员注册的拷贝。然而,人口普查局可能往驾驶员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    xyradius;

public void draw(Canvas c) { // ...

}

}

public class Rectangle extends Shape {

private int    xywidthheight;

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()只能使用Shapelist来调用。它不能,好比说对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的子类而没必要是extendsShape)。咱们说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表明keysV表明vlaues

public class Census {

public static void  addRegistry(Map<String, ? extends Personregistry) ...}

}...

Map<String, DriverallDrivers = ...;

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中,类型参数都只使用一次。返回值的类型既不依赖于类型参数(type parameter)也不依赖于方法的其余参数(这里,只有简单的一个参数)。这告诉咱们类型参数(type argument)被用做多态(polymorphism),它惟一的效果是容许在不一样的调用点,可使用多种实参类型(actual argument)。若是是这种状况,应该使用通配符。通配符就是被设计用来支持灵活的子类化的,这是咱们在这里要强调的。

泛型函数容许类型参数被用来表示方法的一个或多个参数之间的依赖关系,或者参数与其返回值的依赖关系。若是没有这样的依赖关系,不该该使用泛型方法。

(原文:Generic methods allow type parameters to be used to express dependencies among the types of one or more arguments to a method and/or its return type. If there isn’t such a dependency, a generic method should not be used.

前一后的同时使用泛型方法和通配符也是可能的。下面是方法 Collections.copy():

class Collections {

public static <T>  void copy(List<Tdest, List<extends Tsrc){...}

}

注意两个参数的类型的依赖关系。任何被从源list从拷贝出来的对象必须可以将其指定为目标list(dest) 的元素的类型——T类型。所以源类型的元素类型能够是T的任意子类型,咱们不关心具体的类型。

copy方法的签名使用一个类型参数表示了类型依赖,可是使用了一个通配符做为第二个参数的元素类型。咱们也能够用其余方式写这个函数的签名而根本不使用通配符:

class Collections {

public static <T, S extends T>  void copy(List<Tdest, List<Ssrc){...}

}

这也能够,可是第一个类型参数在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的临近的字母,好比S。若是一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用一样的名字来避免混淆。对内部类也是一样。

 

6.            与旧代码交互

直到如今,咱们的例子中都假定了一个理想的世界,那里全部人使用的都是最新版本的java编程语言,它支持泛型。

唉,现实并不是如此。百万行代码都是在早先版本的语言下写做的,他们不可能一夜就转换过来。

后面,在第10部分,咱们会解决把老代码转换为使用泛型的代码的问题。在这里,咱们把注意力放在一个更简单的问题:老代码怎么和泛型代码交互?这个问题包括两部分:在泛型中使用老代码和在老代码中使用泛型代码。

6.1.     在泛型代码中使用老代码

怎样才能使用老代码的同时在本身的代码中享受泛型带来的好处?

做为一个例子,假定你像使用包 com.Fooblibar.widgetsFooblibar.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()时老是使用正确的参数会很棒——就是说,你传进去的确实时一个PartCollection。固然,泛型能够实现这个目的:

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<Partc = new ArrayList<Part>();

c.add(new Guillotine()) ;

c.add(new Blade());

Inventory.addAssembly(”thingee”, c);

Collection<Partk = 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,而kCollection<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的引用来指向一个Stringlist。咱们插入一个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的状况下。

(原文:As a result, the type safety and integrity of the Java virtual machine are never at risk, even in the presence of unchecked warnings.

基本上,擦除去掉了全部的泛型类型信息。全部在尖括号之间的类型信息都被扔掉了,所以,好比说一个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<Partparts) {...}

public static Assembly getAssembly(String name) {...}

}

public interface Assembly {

Collection<PartgetParts(); // 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),而无论他们的实际类型参数。

事实上,泛型之因此为泛型就是由于它对全部其可能的类型参数,它有一样的行为;一样的类能够被看成许多不一样的类型。

做为一个结果,类的静态变量和方法也在全部的实例间共享。这就是为何在静态方法或静态初始化代码中或者在静态变量的声明和初始化时使用类型参数申明是不合法的缘由。

(原文:As consequence, the static variables and methods of a class are also shared among all the instances. That is why it is illegal to refer to the type parameters of a type declaration in a static method or initializer, or in the declaration or initializer of a static variable.

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

数组对象的组成类型不能是一个类型变量或者类型参数,除非它是无上限的通配符类型。你能够声明元素类型是一个类型参数或者参数化类型的数组类型,但不是数组对象(译注:得不到对象,只能声明)。

(原文:The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.

这很烦人,可是确实时这样。为了不下面的状况,必须有这样的限制:

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, 你极可能会问,表明什么?

它表明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<EmpInfoemps=sqlUtility.select(EmpInfo.class, ”select * from emps”); ...

public static <TCollection<Tselect(Class<T>c, String sqlStatement) {

Collection<Tresult = 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的全部元素flushsink 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<Objects;

Collection<Stringcs;

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由于匹配了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!!!

使用这个语法,这个调用是合法的,推断出来的TString,正是咱们想要的。

如今让咱们看一个更现实的例子。一个 java.util.TreeSet<E> 表明一个有序的元素是E类型的树。建立一个TreeSet的一个方法是传递一个 Comparator 对象给构造函数。这个Comparator将会用来按照须要对TreeSet进行排序。

TreeSet(Comparator<Ec)

Comparator 接口是核心:

interface Comparator<T{   int compare(T fst, T snd);   }

假定咱们要建立一个 TreeSet<String> 并传递一个合适的 Comparator,咱们须要传一个能比较StringComparator。这能够是一个 Comparator<String>,也能够是一个 Comparator<Object>。然而咱们不能用Comparator<Object>来调用上面的构造函数。咱们可使用一个有下限的通配符来获得咱们须要的灵活性:

TreeSet(Comparator<super Ec)

这容许任何可用的Comparator被传递进去。

做为使用下限通配符最终的例子,让咱们来看看方法 Collections.max(),它返回一个集合中的最大的元素。

如今,为了让max()能工做,传进来的集合中的全部元素必须实现 Comparatable接口。并且,他们必须都可以被彼此比较(all be comparable to each other)。第一个尝试是:

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

就是说,方法的参数是某一个能和本身进行比较的T的集合。这限制太严格了。

为何?考虑一个能和任何对象进行比较的类型:

class Foo implements Comparable<Object{......

Collection<Foocf = ...;

Collections.max(cf); // 应该能工做

cf 中的每一个元素均可以和每一个cf中的其余元素进行比较,由于每一个这样的元素都是一个Foo,它能够和任意的对象进行比较,也能够和另外一个Foo进行比较。

可是,使用上面的方法签名,咱们发现这个调用被拒绝。推断出来的类型必须是Foo,可是Foo没有实现接口 Comparable<Foo>

精确的(exactly)和本身能比较是不须要的。所须要的是 T可以和它的父类中的一个进行比较,这导出:(注:Collections.max()的实际方法签名更复杂,咱们在第10部分再讨论。)

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

这个推论对大多数想让 Comparable 对任意类型生效的用法中都有效:你老是应该使用 Comparable<? super T>

总之,若是你有一个只使用类型参数T做为参数的API,它的使用应该利用下限通配符( ? super T )的好处。相反的,若是API只返回T,你应该使用上限通配符( ? extends T )来给你的客户端更大的灵活性。

(原文:This reasoning applies to almost any usage of Comparable that is intended to work for arbitrary types: You always want to use Comparable<? super T>.

In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? 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是一个Stringset并不起做用,起做用的是传进来的表达式是一个unknown typeset,它不能保证是一个Stringset或者任何其余的特定类型。

如今,考虑:

class Collections ...

<Tpublic static Set<TunmodifiableSet(Set<Tset) ... }

}...

Set<?s = Collections.unmodifiableSet(unknownSet); // this works! Why?

彷佛这应该不被容许,可是,研究这个特定的调用,容许它是很是安全的。毕竟,unmodifiableSet 确实对任何种类的Set能工做,无论它的元素类型。

由于这种状况相对出现的次数比较多,有一个特殊的规则在能证实代码是安全的状况下容许这样的代码。()这个规则,称为wildcard capture,容许编译器推断出通配符为unknown type做为一个泛型方法的类型参数。

(原文:Because this situation arises relatively frequently, there is a special rule that allows such code under very specific circumstances in which the code can be proven to be safe. This rule, known as wildcard capture, allows the compiler to infer the unknown type of a wildcard as a type argument to a generic method.

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<Ec);

public boolean addAll(Collection<Ec);

}

这固然是类型安全的,可是它不支持这个API的原始契约(original contract)

containsAll() 方法能对全部进来的任意类型的collection工做。它只有在传进来的collection中真正只包含E的实例才成功,可是:

l         传进来的collection的静态类型可能不一样,多是由于调用者不知道传进来的colleciton的精确类型,或者由于它是一个Collection<S>SE的子类型。

l         用一个不一样类型的collection来调用containsAll()应该是合法的。这个例程应该可以工做,返回false

addAll(),咱们应该可以添加任何元素是E的子类型的collection。咱们已经在第5部分讲述了怎么正确的处理这种状况。

 你还应该保证修订过的API保持与老客户端的二进制兼容。者觉得者APIerasure必须与老的未泛型化版本同样。在大多数状况下,这是很天然的结果,可是有些精巧的情形(subtle cases)。咱们看看咱们已经碰到过的精巧的情形中的一个(one of the subtle cases),方法Collections.max()。就像咱们在第9部分看到的,一个似是而非的max()的方法签名是:

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

这很好,除了擦除(erasure)后的签名是:

public static Comparable max(Collection coll)

这和老版本的max() 的签名不一样:

public static Object max(Collection coll)

固然能够把max()定义为这个签名,可是这没有成为现实,由于全部调用了Collections.max()的老的二进制class文件依赖于返回Object的签名。

咱们能够强迫the erasure不一样,经过给形式类型参数T显式的定义一个父类。

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

这是一个对一个类型参数给定多个界限(multiple bounds)的例子,是用语法 T1 & T2 … & Tn。一个有多个界限的类型的参数是全部界限中列出来的类型的子类。当多个界限被使用的时候,界限中的第一个类型被用做这个类型参数的erasure

(原文:This is an example of giving multiple bounds for a type parameter, using the syntax T1& T2 ... & Tn. A type variable with multiple bounds is known to be a subtype of all of the types listed in the bound. When a multiple bound is used, the first type mentioned in the bound is used as the erasure of the type variable.

最后,咱们应该想到max只从传进来collection中读取数据,所以它对元素是T的子类的collection可用。这给咱们JDK中使用的真正的签名:

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

实际中出现那么棘手的问题是很罕见的,可是专业库设计师应该准备好很是仔细的考虑转换现存的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类被从新编译,它不会正确的重载Barcreate()方法,并且,Baz必须被修改,由于Baz的代码被拒绝,它的create的返回值不是Barcreate返回值的子类。(原文: Consequently, unless the class Baz is recompiled, it will not properly override the create() method of Bar.Furthermore, Baz will have to be modified, since the code will be rejected as written - the return type of create() in Baz is not a subtype of the return type of create() in Bar.

(译注:上面的一段话有些莫名其妙,我测试过这个例子,在jdk1.4下,三个类都编译以后改变Bar,只在jdk5下从新编译Bar,而后在jdk5下,Baz仍然可以被使用,固然那,没法使用 Baz b = baz.create();这样的代码。)

11.  致谢

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.

 

终于翻译完了,感受挺累!译文有些地方可能有误或者不许确,因此保留了一些原文,对于重要的地方,也保留原文以助理解,欢迎来信指正或讨论,mailto:chengchengji@163.com

相关文章
相关标签/搜索