Java1.5 泛型指南中文版 (Java1.5 Generic Tutorial): http://blog.csdn.net/explorers/archive/2005/08/15/454837.aspx

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

                                                  译者:  chengchengji@163.com

 

        

 

摘要和关键字

1.         介绍

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.       致谢

 

摘要和关键字

        generics type safe type parameter(variable) formal type parameter actual type parameter wildcards(?) unknown type ? extends T ? super T erasure translation cast instanceof arrays Class Literals as Run-time Type Tokens wildcard capture multiple 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 ,而是一个 Integer List ,写作: 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();

}

这些都应该是很熟悉的,除了尖括号中的部分,那是接口 List Iterator 中的形式类型参数的声明 (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 行。

这产生一个问题:

一个 String List 是一个 Object List 么?大多数人的直觉是回答: 当然!

好,在看下面的几行:

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

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

这里,我们使用 lo 指向 ls 。我们通过 lo 来访问 ls, 一个 String list 。我们可以插入任意对象进去。结果是 ls 中保存的不再是 String 。当我们试图从中取出元素的时候,会得到意外的结果。

java 编译器当然会阻止这种情况的发生。第 2 行会导致一个编译错误。

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

这可能是你学习泛型中最难理解的部分,因为它和你的直觉相反。

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

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

}

}

所有的图形通常都有很多个形状。假定它们用一个 list 来表示, Canvas 里有一个方法来画出所有的形状会比较方便:

        public   void  drawAll(List<Shape> shapes) {

             for  (Shape s : shapes) {

                s.draw( this );

          }

}

现在,类型规则导致 drawAll() 只能使用 Shape list 来调用。它不能,比如说对 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 的子类而不必是 extends Shape) 。我们说 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 代表 keys V 代表 vlaues

public  class  Census  {

public  static void   addRegistry(Map < String, ? extends Person >  registry)  {  ... }

} ...

Map < String, Driver >  allDrivers = ...;

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 中,类型参数 T  都只使用一次。返回值的类型既不依赖于类型参数 (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 isnt such a dependency, a generic method should not be used.

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

class   Collections  {

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

}

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

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

class   Collections  {

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

}

这也可以, 但是 第一个类型参数在 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  代表类型,无论何时都没有比这更具体的类型来区分它。这经常见于泛型方法。如果有多个类型参数,我们可能使用字母表中 T 的临近的字母,比如 S 。如果一个泛型函数在一个泛型类里边出现,最好避免在方法的类型参数和类的类型参数中使用同样的名字来避免混淆。对内部类也是同样。

 

6.             与旧代码交互

直到现在,我们的例子中都假定了一个理想的世界,那里所有人使用的都是最新版本的 java 编程语言,它支持泛型。

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

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

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

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

作为一个 例子 ,假定你像使用包   com.Fooblibar.widgets Fooblibar.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() 时总是使用正确的参数会很棒——就是说,你传进去的确实时一个 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(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>  类型。这可以工作,但是为什么?毕竟,大多数集合不包含 Part 对象,而且总的来说,编译器无法知道 Collection 指的是什么类型的集合。

在严格的泛型代码里, Collection 应该总是带着类型参数。当一个泛型类型,比如 Collection 被使用而没有类型参数时,它被称作一个 raw type( 自然类型 ??)

大多数人的第一直觉时 Collection 实际上意味着  Collection<Object> 。但是,像我们前面看到的,当需要 Collection<Object> 时传递  Collection<Part> 是不安全的。类型 Collection 表示一个未知类型元素的集合,就像 Collection<?> ,这样说更准确。

但是等一下,那也不正确。考虑 getParts() 这个调用,它返回一个 Collection 。然后它被赋值给 k ,而 k Collection<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 的引用来指向一个 String list 。我们插入一个 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 < Part >  parts)  { ... }

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

}

public  interface  Assembly  {

Collection < Part >  getParts( ); // 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,  你很可能会问, 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 < 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;

}

来通过一种类型安全的方式得到我们要的集合。

这项技术是一个非常有用的技巧,它已成为一个在处理注释 ( annotations ) 的新 API 中被广泛使用的习惯用法。

 

 

9.             More fun with *

在这一部分,我们来考虑一些通配符得高级用法。我们已经看到了上限通配符在从一个数据结构中进行读取的几个例子。现在考虑相反的情况,一个只写的数据结构。

接口 Sink 是这种情况的一个简单例子。

               interface  Sink<T> {

                void  flush(T t);

    }

我们可以想象他被如下面的代码一样使用。方法 writeAll()  被设计来把集合 coll 的所有元素 flush sink 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 < Object >  s;

Collection < String >  cs;

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 因为 T  匹配了 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!!!

使用这个语法,这个调用是合法的,推断出来的 T 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 ,我们需要传一个能比较 String Comparator 。这可以是一个  Comparator<String> ,也可以是一个  Comparator<Object> 。然而我们不能用 Comparator<Object> 来调用上面的构造函数。我们可以使用一个有下限的通配符来得到我们需要的灵活性:

TreeSet( Comparator < ? super E >  c)

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

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

现在,为了让 max() 能工作,传进来的集合中的所有元素必须实现  Comparatable 接口。而且,他们必须都能够被彼此比较( all be comparable to each other )。第一个尝试是:

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

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

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

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

Collection < Foo >  cf = ...;

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

cf  中的每个元素都可以和每个 cf 中的其他元素进行比较,因为每个这样的元素都是一个 Foo, 它可以和任意的对象进行比较,也可以和另一个 Foo 进行比较。

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

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

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

这个推论对大多数想让  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 是一个 String set 并不起作用,起作用的是传进来的表达式是一个 unknown type set ,它不能保证是一个 String set 或者任何其他的特定类型。

现在,考虑:

class   Collections  {  ...

< T >  public static  Set < T >  unmodifiableSet( Set < T >  set)  {  ...  }

} ...

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 < E >  c);

public  boolean  addAll(Collection < E >  c);

}

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

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

l          传进来的 collection 的静态类型可能不同,可能是因为调用者不知道传进来的 colleciton 的精确类型,或者因为它是一个 Collection<S> S E 的子类型。

l          用一个不同类型的 collection 来调用 containsAll() 应该是合法的。这个例程应该能够工作,返回 false

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

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

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

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

public  static  Comparable max(Collection coll)

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

public  static  Object max(Collection coll)

当然可以把 max() 定义为这个签名,但是这没有成为现实,因为所有调用了 Collections.max() 的老的二进制 class 文件依赖于返回 Object 的签名。

我们可以强迫 the erasure 不同,通过给形式类型参数 T 显式的定义一个父类。

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

这是一个对一个类型参数给定多个界限 (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  < T extends Object & Comparable < ? super T >>  T max(Collection < ? extends T >  coll)

实际中出现那么棘手的问题是很罕见的,但是专业库设计师应该准备好非常仔细的考虑转换现存的 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 类被重新编译,它不会正确的重载 Bar create() 方法,而且, Baz 必须被修改,因为 Baz 的代码被拒绝,它的 create 的返回值不是 Bar create 返回值的子类。(原文:   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