北京北大青鸟学校学术部讲解:Java类库中的泛型(一)

北京北大青鸟学校学术部讲师将继续讲解关于Java泛型的技术知识,关于Java泛型的定义、相关例子等请参考之前的几篇文章,在这里就不做陈述了。今天这篇文章,将主讲介绍一下Java类库中的泛型(北大青鸟课程

集合类
北京北大青鸟学校专家介绍:到目前为止,Java 类库中泛型支持存在最多的地方就是集合框架。就像容器类是 C++ 语言中模板的主要动机一样(参阅 附录 A:与 C++ 模板的比较)(尽管它们随后用于很多别的用途),改善集合类的类型安全是 Java 语言中泛型的主要动机。集合类也充当如何使用泛型的模型,因为它们演示了泛型的几乎所有的标准技巧和方言。(北大青鸟课程

所有的标准集合接口都是泛型化的 —— Collection、List、Set 和 Map。类似地,集合接口的实现都是用相同类型参数泛型化的,所以 HashMap 实现 Map 等。

集合类也使用泛型的许多“技巧”和方言,比如上限通配符和下限通配符。例如,在接口 Collection 中,addAll 方法是像下面这样定义的:
interface Collection {
boolean addAll(Collection);
}

该定义组合了通配符类型参数和有限制类型参数,允许您将 Collection 的内容添加到 Collection。(北大青鸟课程

北京北大青鸟学校专家提醒:如果类库将 addAll() 定义为接受 Collection,您就不能将 Collection 的内容添加到 Collection。不是限制 addAll() 的参数是一个与您将要添加到的集合包含相同类型的集合,而有可能建立一个更合理的约束,即传递给 addAll() 的集合的元素 适合于添加到您的集合。有限制类型允许您这样做,并且使用有限制通配符使您不需要使用另一个不会用在其他任何地方的占位符名称。

应该可以将 addAll() 的类型参数定义为 Collection。但是,北京北大青鸟学校专家认为,这不但没什么用,而且还会改变 Collection 接口的语义,因为泛型版本的语义将会不同于非泛型版本的语义。这阐述了泛型化一个现有的类要比定义一个新的泛型类难得多,因为您必须注意不要更改类的语义或者破坏现有的非泛型代码。

作为泛型化一个类(如果不小心的话)如何会更改其语义的一个更加微妙的例子,注意 Collection.removeAll() 的参数的类型是 Collection,而不是 Collection。这是因为传递混合类型的集合给 removeAll() 是可接受的,并且更加限制地定义 removeAll 将会更改方法的语义和有用性。(北大青鸟课程
 
其他容器类
北京北大青鸟学校专家介绍:除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。它们都已经在其包含的值的类型上泛型化了,所以 WeakReference 是对 T 类型的对象的弱引用,ThreadLocal 则是到 T 类型的线程局部变量的句柄。

泛型不止用于容器
泛型最常见最直观的使用是容器类,比如集合类或引用类(比如 WeakReference)。Collection 中类型参数的含义很明显 —— “一个所有值都是 V 类型的集合”。类似地,ThreadLocal 也有一个明显的解释 —— “一个其类型是 T 的线程局部变量”。但是,泛型规格说明中没有指定容积。(北大青鸟课程

像 Comparable 或 Class 这样的类中类型参数的含义更加微妙。有时,就像 Class 中一样,类型变量主要是帮助编译器进行类型推理。有时,就像隐含的 Enum> 中一样,类型变量只是在类层次结构上加一个约束。

Comparable
Comparable 接口已经泛型化了,所以实现 Comparable 的对象声明它可以与什么类型进行比较。北京北大青鸟学校专家总结:(通常,这是对象本身的类型,但是有时也可能是父类。)
public interface Comparable {
public boolean compareTo(T other);
}

所以 Comparable 接口包含一个类型参数 T,该参数是一个实现 Comparable 的类可以与之比较的对象的类型。这意味着如果定义一个实现 Comparable 的类,比如 String,就必须不仅声明类支持比较,还要声明它可与什么比较(通常是与它本身比较):
public class String implements Comparable { ... }

现在来考虑一个二元 max() 方法的实现。您想要接受两个相同类型的参数,二者都是 Comparable,并且相互之间是 Comparable。幸运的是,如果使用泛型方法和有限制类型参数的话,这相当直观:
public static > T max(T t1, T t2) {
if (t1.compareTo(t2) > 0)
    return t1;
else
    return t2;
}

北京北大青鸟学校专家介绍:在本例中,您定义了一个泛型方法,在类型 T 上泛型化,您约束该类型扩展(实现) Comparable。两个参数都必须是 T 类型,这表示它们是相同类型,支持比较,并且相互可比较。容易!

更好的是,编译器将使用类型推理来确定当调用 max() 时 T 的值表示什么意思。所以根本不用指定 T,下面的调用就能工作:
String s = max("moo", "bark");

编译器将计算出 T 的预定值是 String,因此它将进行编译和类型检查。但是如果您试图用不实现 Comparable 的 类 X 的参数调用 max(),那么编译器将不允许这样做。
 
Class
类 Class 已经泛型化了,但是很多人一开始都感觉其泛型化的方式很混乱。Class 中类型参数 T 的含义是什么?事实证明它是所引用的类接口。怎么会是这样的呢?那是一个循环推理?如果不是的话,为什么这样定义它?

在以前的 JDK 中,Class.newInstance() 方法的定义返回 Object,您很可能要将该返回类型强制转换为另一种类型:
class Class {
Object newInstance();
}
但是使用泛型,您定义 Class.newInstance() 方法具有一个更加特定的返回类型:
class Class {
T newInstance();
}(北大青鸟课程

如何创建一个 Class 类型的实例?北京北大青鸟学校专家介绍,就像使用非泛型代码一样,有两种方式:调用方法 Class.forName() 或者使用类常量 X.class。Class.forName() 被定义为返回 Class。另一方面,类常量 X.class 被定义为具有类型 Class,所以 String.class 是 Class 类型的。

让 Foo.class 是 Class 类型的有什么好处?北京北大青鸟学校专家解答:最大的好处是,通过类型推理的魔力,可以提高使用反射的代码的类型安全。另外,还不需要将 Foo.class.newInstance() 强制类型转换为 Foo。

考虑一个方法,它从数据库检索一组对象,并返回 JavaBeans 对象的一个集合。您通过反射来实例化和初始化创建的对象,但是这并不意味着类型安全必须完全被抛至脑后。考虑下面这个方法:
public static List getRecords(Class c, Selector s) {
// Use Selector to select rows
List list = new ArrayList();
for (/* iterate over results */) {
    T row = c.newInstance();
    // use reflection to set fields from result
    list.add(row);
}
return list;
}
北大青鸟课程
可以像下面这样简单地调用该方法:
List l = getRecords(FooRecord.class, fooSelector);
编译器将会根据 FooRecord.class 是 Class 类型的这一事实,推断 getRecords() 的返回类型。您使用类常量来构造新的实例并提供编译器在类型检查中要用到的类型信息。
 
用 Class 替换 T[]
Collection 接口包含一个方法,用于将集合的内容复制到一个调用者指定类型的数组中:
public Object[] toArray(Object[] prototypeArray) { ... }

toArray(Object[]) 的语义是,如果传递的数组足够大,就会使用它来保存结果,否则,就会使用反射分配一个相同类型的新数组。一般来说,单独传递一个数组作为参数来提供想要的返回类型是一个小技巧,但是在引入泛型之前,这是与方法交流类型信息最方便的方式。

有了泛型,就可以用一种更加直观的方式来做这件事。不像上面这样定义 toArray(),泛型 toArray() 可能看起来像下面这样:
public T[] toArray(Class returnType)
调用这样一个 toArray() 方法很简单:
FooBar[] fba = something.toArray(FooBar.class);

Collection 接口还没有改变为使用该技术,因为这会破坏许多现有的集合实现。但是如果使用泛型从新构建 Collection,则当然会使用该方言来指定它想要返回值是哪种类型。
 
Enum
JDK 5.0 中 Java 语言另一个增加的特性是枚举。当您使用 enum 关键字声明一个枚举时,编译器就会在内部为您生成一个类,用于扩展 Enum 并为枚举的每个值声明静态实例。所以如果您说:
public enum Suit {HEART, DIAMOND, CLUB, SPADE};

编译器就会在内部生成一个叫做 Suit 的类,该类扩展 java.lang.Enum 并具有叫做 HEART、DIAMOND、CLUB 和 SPADE 的常量(public static final)成员,每个成员都是 Suit 类。(北大青鸟课程)

与 Class 一样,Enum 也是一个泛型类。但是与 Class 不同,它的签名稍微更复杂一些:
class Enum> { . . . }
这究竟是什么意思?这难道不会导致无限递归?

我们逐步来分析。类型参数 E 用于 Enum 的各种方法中,比如 compareTo() 或 getDeclaringClass()。为了这些方法的类型安全,Enum 类必须在枚举的类上泛型化。

所以 extends Enum 部分如何理解?

北京北大青鸟学校专家介绍,该部分又具有两个部分。第一部分指出,作为 Enum 的类型参数的类本身必须是 Enum 的子类型,所以您不能声明一个类 X 扩展 Enum。第二部分指出,任何扩展 Enum 的类必须传递它本身 作为类型参数。您不能声明 X 扩展 Enum,即使 Y 扩展 Enum。

总之,Enum 是一个参数化的类型,只可以为它的子类型实例化,并且这些子类型然后将根据子类型来继承方法。幸运的是,在 Enum 情况下,编译器为您做这些工作,一切都很好。(北京北大青鸟学校,未完待续)
 

北大青鸟网上报名
北大青鸟招生简章